Repository: pop-os/cosmic-files Branch: master Commit: 750c92c8412a Files: 152 Total size: 1.4 MB Directory structure: gitextract_2jlip0v_/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yml │ └── validate-desktop-files.yml ├── .gitignore ├── .zed/ │ └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── TESTING.md ├── build.rs ├── cosmic-files-applet/ │ ├── Cargo.toml │ └── src/ │ ├── file_manager.rs │ └── main.rs ├── debian/ │ ├── changelog │ ├── control │ ├── copyright │ ├── rules │ └── source/ │ ├── format │ └── options ├── examples/ │ ├── copy.rs │ ├── desktop.rs │ ├── dialog.rs │ ├── gio-list.rs │ ├── gio-mount.rs │ └── gvfs.rs ├── i18n/ │ ├── af/ │ │ └── cosmic_files.ftl │ ├── ar/ │ │ └── cosmic_files.ftl │ ├── be/ │ │ └── cosmic_files.ftl │ ├── bg/ │ │ └── cosmic_files.ftl │ ├── bn/ │ │ └── cosmic_files.ftl │ ├── ca/ │ │ └── cosmic_files.ftl │ ├── cs/ │ │ └── cosmic_files.ftl │ ├── da/ │ │ └── cosmic_files.ftl │ ├── de/ │ │ └── cosmic_files.ftl │ ├── el/ │ │ └── cosmic_files.ftl │ ├── en/ │ │ └── cosmic_files.ftl │ ├── en-GB/ │ │ └── cosmic_files.ftl │ ├── eo/ │ │ └── cosmic_files.ftl │ ├── es/ │ │ └── cosmic_files.ftl │ ├── es-419/ │ │ └── cosmic_files.ftl │ ├── es-MX/ │ │ └── cosmic_files.ftl │ ├── et/ │ │ └── cosmic_files.ftl │ ├── eu/ │ │ └── cosmic_files.ftl │ ├── fa/ │ │ └── cosmic_files.ftl │ ├── fi/ │ │ └── cosmic_files.ftl │ ├── fr/ │ │ └── cosmic_files.ftl │ ├── fy/ │ │ └── cosmic_files.ftl │ ├── ga/ │ │ └── cosmic_files.ftl │ ├── gd/ │ │ └── cosmic_files.ftl │ ├── gu/ │ │ └── cosmic_files.ftl │ ├── he/ │ │ └── cosmic_files.ftl │ ├── hi/ │ │ └── cosmic_files.ftl │ ├── hr/ │ │ └── cosmic_files.ftl │ ├── hu/ │ │ └── cosmic_files.ftl │ ├── id/ │ │ └── cosmic_files.ftl │ ├── ie/ │ │ └── cosmic_files.ftl │ ├── is/ │ │ └── cosmic_files.ftl │ ├── it/ │ │ └── cosmic_files.ftl │ ├── ja/ │ │ └── cosmic_files.ftl │ ├── jv/ │ │ └── cosmic_files.ftl │ ├── ka/ │ │ └── cosmic_files.ftl │ ├── kab/ │ │ └── cosmic_files.ftl │ ├── kk/ │ │ └── cosmic_files.ftl │ ├── kmr/ │ │ └── cosmic_files.ftl │ ├── kn/ │ │ └── cosmic_files.ftl │ ├── ko/ │ │ └── cosmic_files.ftl │ ├── li/ │ │ └── cosmic_files.ftl │ ├── lt/ │ │ └── cosmic_files.ftl │ ├── ml/ │ │ └── cosmic_files.ftl │ ├── ms/ │ │ └── cosmic_files.ftl │ ├── nb-NO/ │ │ └── cosmic_files.ftl │ ├── nl/ │ │ └── cosmic_files.ftl │ ├── nn/ │ │ └── cosmic_files.ftl │ ├── oc/ │ │ └── cosmic_files.ftl │ ├── pa/ │ │ └── cosmic_files.ftl │ ├── pl/ │ │ └── cosmic_files.ftl │ ├── pt/ │ │ └── cosmic_files.ftl │ ├── pt-BR/ │ │ └── cosmic_files.ftl │ ├── ro/ │ │ └── cosmic_files.ftl │ ├── ru/ │ │ └── cosmic_files.ftl │ ├── sk/ │ │ └── cosmic_files.ftl │ ├── sl/ │ │ └── cosmic_files.ftl │ ├── sr/ │ │ └── cosmic_files.ftl │ ├── sr-Cyrl/ │ │ └── cosmic_files.ftl │ ├── sr-Latn/ │ │ └── cosmic_files.ftl │ ├── sv/ │ │ └── cosmic_files.ftl │ ├── ta/ │ │ └── cosmic_files.ftl │ ├── th/ │ │ └── cosmic_files.ftl │ ├── ti/ │ │ └── cosmic_files.ftl │ ├── tr/ │ │ └── cosmic_files.ftl │ ├── uk/ │ │ └── cosmic_files.ftl │ ├── uz/ │ │ └── cosmic_files.ftl │ ├── vi/ │ │ └── cosmic_files.ftl │ ├── yue-Hant/ │ │ └── cosmic_files.ftl │ ├── zh-CN/ │ │ └── cosmic_files.ftl │ └── zh-TW/ │ └── cosmic_files.ftl ├── i18n.toml ├── justfile ├── res/ │ ├── com.system76.CosmicFiles.desktop │ └── com.system76.CosmicFiles.metainfo.xml ├── rust-toolchain.toml ├── rustfmt.toml ├── samples/ │ ├── i18n/ │ │ ├── منزل │ │ └── 主目錄 │ ├── mime/ │ │ ├── application/ │ │ │ ├── javascript.js │ │ │ ├── octet-stream │ │ │ ├── x-shellscript.sh │ │ │ └── x-yaml.yaml │ │ ├── audio/ │ │ │ └── x-vorbis+ogg.ogg │ │ ├── check.sh │ │ └── text/ │ │ ├── css.css │ │ ├── csv.csv │ │ ├── html.html │ │ ├── markdown.md │ │ ├── plain.txt │ │ ├── rust.rs │ │ ├── x-chdr.h │ │ └── x-csrc.c │ └── mode/ │ ├── .gitignore │ └── create.sh ├── scripts/ │ └── copy.sh └── src/ ├── app.rs ├── archive.rs ├── channel.rs ├── clipboard.rs ├── config.rs ├── context_action.rs ├── dialog.rs ├── key_bind.rs ├── large_image.rs ├── lib.rs ├── load_image.rs ├── localize.rs ├── main.rs ├── menu.rs ├── mime_app.rs ├── mime_icon.rs ├── mounter/ │ ├── gvfs.rs │ └── mod.rs ├── mouse_area.rs ├── operation/ │ ├── controller.rs │ ├── mod.rs │ ├── notifiers.rs │ ├── reader.rs │ └── recursive.rs ├── spawn_detached.rs ├── tab.rs ├── thumbnail_cacher.rs ├── thumbnailer.rs ├── trash.rs └── zoom.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ **Cosmic-files version:** **Issue/Bug description:** **Steps to reproduce:** **Expected behavior:** **Other notes:** ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ - [ ] 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. - [ ] I understand these changes in full and will be able to respond to review comments. - [ ] My change is accurately described in the commit message. - [ ] My contribution is tested and working as described. - [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. ================================================ FILE: .github/workflows/ci.yml ================================================ name: Cargo Build & Test on: push: branches: - master pull_request: env: CARGO_TERM_COLOR: always jobs: test: name: cosmic-files - latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: sudo apt-get update; sudo apt-get install libclang-dev libglib2.0-dev libxkbcommon-dev - run: rustup update stable && rustup default stable - run: cargo test --verbose --no-default-features - run: cargo test --verbose - run: cargo test --verbose --all-features ================================================ FILE: .github/workflows/validate-desktop-files.yml ================================================ name: Validate .desktop files on: push: branches: - master pull_request: jobs: validate: runs-on: ubuntu-latest container: image: ubuntu:25.10 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install desktop-file-utils run: | apt-get update apt-get install -y desktop-file-utils findutils - name: Validate .desktop files run: | set -e echo "Checking for .desktop files..." files=$(find . -type f -name "*.desktop") if [ -z "$files" ]; then echo "No .desktop files found." exit 0 fi echo "$files" | while read -r file; do echo "Validating: $file" desktop-file-validate "$file" done ================================================ FILE: .gitignore ================================================ /.cargo/ /debian/*debhelper* /debian/cosmic-files.substvars /debian/cosmic-files/ /debian/files /flamegraph.svg /heaptrack.* /perf.* /target/ /test/ /vendor.tar /vendor/ ================================================ FILE: .zed/settings.json ================================================ { "format_on_save": "on", "lsp": { "rust-analyzer": { "initialization_options": { "check": { "command": "clippy", }, "rustfmt": { "extraArgs": ["+nightly"], }, }, }, }, } ================================================ FILE: Cargo.toml ================================================ [package] name = "cosmic-files" version = "1.0.12" authors = ["Jeremy Soller "] edition = "2024" license = "GPL-3.0-only" rust-version = "1.93" [dependencies] anyhow = "1" jiff = "0.2" jiff-icu = "0.2" icu = { version = "2.2.0", features = ["compiled_data"] } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true } dirs = "6.0.0" gio = { version = "0.21", optional = true } glib = { version = "0.21", optional = true } glob = "0.3" ignore = "0.4" image = "0.25" libc = "0.2" log = "0.4" mime_guess = "2" notify-debouncer-full = "0.7" notify-rust = { version = "4", optional = true } open = "5.3.4" paste = "1.0" regex = "1" rustc-hash = "2.1" serde = { version = "1", features = ["serde_derive"] } shlex = { version = "1.3" } tempfile = "3" tikv-jemallocator = { version = "0.6", optional = true } 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.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.45" lzma-rust2 = { version = "0.16", optional = true } ordermap = { version = "1.2.0", features = ["serde"] } # Internationalization i18n-embed = { version = "0.16", features = [ "fluent-system", "desktop-requester", ] } i18n-embed-fl = "0.10" rust-embed = "8" slotmap = "1.1.1" recently-used-xbel = "1.2.0" 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] version = "0.18" default-features = false features = ["fs", "io", "macros", "polling", "runtime"] [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" default-features = false #TODO: a11y feature crashes features = [ "about", "advanced-shaping", "autosize", "multi-window", "tokio", "winit", "surface-message", ] [[example]] name = "gio-list" required-features = ["gvfs"] [[example]] name = "gio-mount" required-features = ["gvfs"] [[example]] name = "gvfs" required-features = ["gvfs"] [features] default = [ "bzip2", "dbus-config", "desktop", "gvfs", "io-uring", "lzma-rust2", "notify", "wayland", "wgpu", ] dbus-config = ["libcosmic/dbus-config"] desktop = ["libcosmic/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"] [profile.dev] opt-level = 1 [profile.release-with-debug] inherits = "release" debug = true [target.'cfg(unix)'.dependencies] fork = "0.7" uzers = "0.12.2" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.18" [build-dependencies] xdgen = "0.1" [dev-dependencies] # cap-std = "3" # cap-tempfile = "3" 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" } # [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" } [workspace] members = ["cosmic-files-applet"] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # cosmic-files File manager for the COSMIC desktop environment ## Build the project from source ```sh # Clone the project using `git` git clone https://github.com/pop-os/cosmic-files # Change to the directory that was created by `git` cd cosmic-files # Build an optimized version using `cargo`, this may take a while cargo build --release # Run the optimized version using `cargo` cargo run --release ``` ## Community and Contributing The COSMIC desktop environment is maintained by System76 for use in Pop!_OS. A list of all COSMIC projects can be found in the [cosmic-epoch](https://github.com/pop-os/cosmic-epoch) project's README. If you would like to discuss COSMIC and Pop!_OS, please consider joining the [Pop!_OS Chat](https://chat.pop-os.org/). More information and links can be found on the [Pop!_OS Website](https://pop.system76.com). ## License This project is licensed under [GPLv3](LICENSE) ================================================ FILE: TESTING.md ================================================ # Testing This document provides a regression testing checklist for COSMIC Files. The checklist provides a starting point for Quality Assurance reviews. ## Checklist ### Basic navigation - [ ] Middle-click opens directory in a new tab (not focused). - [ ] Open two scrollable tabs. Scroll one tab, then switch to the other tab; it should not have scrolled. - [ ] Hover over the top item in the folder, then scroll down until it's out of view (while still hovered). On scrolling back up (with the mouse in a different position), the item should not have the hover highlight. - [ ] Right-click an item in the sidebar. No visual change should occur with the rest of the items. - [ ] Remove an item from the sidebar, then re-pin it. ### File operations - [ ] Right-click -> Create a new folder, then enter it. - [ ] Right-click in the empty folder -> Create a new file. - [ ] Navigate to the parent folder, create another new file, then drag it into the created folder. - [ ] Files can be renamed. - [ ] Files can be opened with non-default apps & browsing store for new apps works. - [ ] Normal right-click shows `Move to trash` option. - [ ] Shift right-click, and right-click followed by Shift, both show `Permanently delete` option. ### Advanced navigation & view settings - [ ] Image and video thumbnails generate & display in local folders. - [ ] Gallery preview shows with Spacebar. - [ ] Details pane shows with Ctrl+Spacebar. - [ ] Zoom in/out and reset to default zoom work. - [ ] Ctrl+1 and Ctrl+2 switch between list and icon view. - [ ] Ctrl+H shows/hides hidden files. - [ ] Directories can be sorted at top or inline. - [ ] Settings -> Theme works. - [ ] Settings -> Type to Search affects behavior as designed. - [ ] Single-click to open setting takes effect. - [ ] Sorting options work. - [ ] Cutting, copying, and pasting files works. - [ ] F5 reloads current directory. - [ ] Left sidebar can be collapsed and expanded. ### External filesystems - [ ] Add a network drive (e.g. SFTP) and navigate into it. - [ ] Plug in a USB drive; able to mount, browse, and eject. ### Integrations - [ ] Desktop icons display as expected - [ ] Drag-and-drop into Firefox works ================================================ FILE: build.rs ================================================ use std::path::PathBuf; use std::{env, fs}; use xdgen::{App, Context, FluentString}; fn main() { let id = "com.system76.CosmicFiles"; let ctx = Context::new("i18n", env::var("CARGO_PKG_NAME").unwrap()).unwrap(); let app = App::new(FluentString("cosmic-files")) .comment(FluentString("comment")) .keywords(FluentString("keywords")); let output = PathBuf::from("target/xdgen"); fs::create_dir_all(&output).unwrap(); fs::write( output.join(format!("{}.desktop", id)), app.expand_desktop(format!("res/{}.desktop", id), &ctx) .unwrap(), ) .unwrap(); fs::write( output.join(format!("{}.metainfo.xml", id)), app.expand_metainfo(format!("res/{}.metainfo.xml", id), &ctx) .unwrap(), ) .unwrap(); } ================================================ FILE: cosmic-files-applet/Cargo.toml ================================================ [package] name = "cosmic-files-applet" version = "1.0.12" edition = "2024" [dependencies] log = "0.4" zbus = "4" # Blocking connection in zbus 5 hangs [dependencies.cosmic-files] path = ".." default-features = false features = ["desktop", "gvfs", "wayland", "desktop-applet"] ================================================ FILE: cosmic-files-applet/src/file_manager.rs ================================================ // SPDX-License-Identifier: GPL-3.0-only // Implementation of https://www.freedesktop.org/wiki/Specifications/file-manager-interface/ #![allow(dead_code, non_snake_case)] use std::process; pub struct FileManager; impl FileManager { //TODO: return error? fn open(&self, uris: &[&str], _startup_id: &str) { match process::Command::new("cosmic-files").args(uris).spawn() { Ok(mut child) => { log::info!("spawned cosmic-files with id {:?}", child.id()); match child.wait() { Ok(status) => { if status.success() { log::info!("cosmic-files exited with {status}"); } else { log::warn!("failed to run cosmic-files: exited with {status}"); } } Err(err) => { log::warn!("failed to run cosmic-files: {err}"); } } } Err(err) => { log::warn!("failed to spawn cosmic-files: {err}"); } } } } //TODO: why does &[&str] not implement Deserialize? #[zbus::interface(name = "org.freedesktop.FileManager1")] impl FileManager { fn ShowFolders(&self, URIs: Vec<&str>, StartupId: &str) { log::warn!("ShowFolders {:?} {:?}", URIs, StartupId); self.open(&URIs, StartupId) } fn ShowItems(&self, URIs: Vec<&str>, StartupId: &str) { log::warn!("ShowItems {:?} {:?}", URIs, StartupId); self.open(&URIs, StartupId) } fn ShowItemProperties(&self, URIs: Vec<&str>, StartupId: &str) { log::warn!("ShowItemProperties {:?} {:?}", URIs, StartupId); self.open(&URIs, StartupId) } } ================================================ FILE: cosmic-files-applet/src/main.rs ================================================ mod file_manager; fn main() -> Result<(), Box> { //TODO: move file manager service to its own daemon? let _conn_res = zbus::blocking::connection::Builder::session()? .name("org.freedesktop.FileManager1")? .serve_at("/org/freedesktop/FileManager1", file_manager::FileManager)? .build(); cosmic_files::desktop() } ================================================ FILE: debian/changelog ================================================ cosmic-files (1.0.12) noble; urgency=medium * Epoch 1.0.12 version update -- Jeremy Soller Tue, 05 May 2026 10:23:57 -0600 cosmic-files (1.0.11) noble; urgency=medium * Epoch 1.0.11 version update -- Jeremy Soller Tue, 14 Apr 2026 11:09:44 -0600 cosmic-files (1.0.9) noble; urgency=medium * Epoch 1.0.9 version update -- Jeremy Soller Mon, 06 Apr 2026 15:10:13 -0600 cosmic-files (1.0.8) noble; urgency=medium * Epoch 1.0.8 version update -- Jeremy Soller Mon, 23 Feb 2026 08:13:12 -0700 cosmic-files (1.0.7) noble; urgency=medium * Epoch 1.0.7 version update -- Jeremy Soller Tue, 17 Feb 2026 07:58:35 -0700 cosmic-files (1.0.6) noble; urgency=medium * Epoch 1.0.6 version update -- Jeremy Soller Thu, 05 Feb 2026 15:23:07 -0700 cosmic-files (1.0.5) noble; urgency=medium * Epoch 1.0.5 version update -- Jeremy Soller Fri, 30 Jan 2026 17:16:28 -0700 cosmic-files (1.0.4) noble; urgency=medium * Epoch 1.0.4 version update -- Jeremy Soller Wed, 21 Jan 2026 10:16:11 -0700 cosmic-files (1.0.0) jammy; urgency=medium * Stable release. -- Jeremy Soller Mon, 29 Dec 2025 15:12:39 -0700 cosmic-files (0.1.0) jammy; urgency=medium * Initial release. -- Jeremy Soller Wed, 03 Jan 2024 13:38:21 -0700 ================================================ FILE: debian/control ================================================ Source: cosmic-files Section: admin Priority: optional Maintainer: Jeremy Soller Build-Depends: debhelper-compat (=13), git, just (>= 1.13.0), libclang-dev, libglib2.0-dev, libxkbcommon-dev, pkg-config, rust-all, Standards-Version: 4.6.2 Homepage: https://github.com/pop-os/cosmic-files Package: cosmic-files Architecture: amd64 arm64 Depends: ${misc:Depends}, ${shlibs:Depends}, xdg-utils Description: Cosmic File Manager ================================================ FILE: debian/copyright ================================================ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: cosmic-files Upstream-Contact: Jeremy Soller Source: https://github.com/pop-os/cosmic-files Files: * Copyright: System76 License: GPL-3.0 ================================================ FILE: debian/rules ================================================ #!/usr/bin/make -f export DESTDIR = debian/cosmic-files export VENDOR ?= 1 %: dh $@ override_dh_auto_clean: if ! ischroot && test "${VENDOR}" = "1"; then \ just vendor; \ fi override_dh_auto_build: just build-vendored override_dh_auto_install: just rootdir=$(DESTDIR) install ================================================ FILE: debian/source/format ================================================ 3.0 (native) ================================================ FILE: debian/source/options ================================================ tar-ignore=.github tar-ignore=.vscode tar-ignore=vendor tar-ignore=target ================================================ FILE: examples/copy.rs ================================================ 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> { let mut context = Context::new(Controller::default()) .on_progress(|op, progress| { println!("{:?}: {:?}", op.to, progress); }) .on_replace(|op, conflicting_count| { Box::pin(async move { println!( "replace {:?}? (y/N) [conflicting: {}]", op.to, conflicting_count ); let mut line = String::new(); match io::stdin().read_line(&mut line) { Ok(_) => { if line == "y" { ReplaceResult::Replace(false) } else { ReplaceResult::Skip(false) } } Err(err) => { eprintln!("failed to read stdin: {}", err); ReplaceResult::Cancel } } }) }); context .recursive_copy_or_move( vec![(PathBuf::from("test/a"), PathBuf::from("test/b"))], Method::Copy, ) .await?; context .recursive_copy_or_move( vec![(PathBuf::from("test/b"), PathBuf::from("test/c"))], Method::Move { cross_device_copy: false, }, ) .await?; Ok(()) } ================================================ FILE: examples/desktop.rs ================================================ // This launches the desktop mode as a regular window for easier testing. fn main() -> Result<(), Box> { cosmic_files::desktop() } ================================================ FILE: examples/dialog.rs ================================================ 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> { 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::(settings, ())?; Ok(()) } #[derive(Clone, Debug)] pub enum Message { DialogMessage(DialogMessage), DialogOpen(DialogKind), DialogOpenImages, DialogResult(DialogResult), } pub struct App { core: Core, dialog_opt: Option>, result_opt: Option, } impl Application for App { type Executor = executor::Default; type Flags = (); type Message = Message; const APP_ID: &'static str = "com.system76.CosmicFilesDialogExample"; fn core(&self) -> &Core { &self.core } fn core_mut(&mut self) -> &mut Core { &mut self.core } fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { ( Self { core, dialog_opt: None, result_opt: None, }, Task::none(), ) } fn update(&mut self, message: Message) -> Task { match message { Message::DialogMessage(dialog_message) => { if let Some(dialog) = &mut self.dialog_opt { return dialog.update(dialog_message); } } Message::DialogOpen(dialog_kind) => { if self.dialog_opt.is_none() { let (mut dialog, task) = Dialog::new( DialogSettings::new().kind(dialog_kind), Message::DialogMessage, Message::DialogResult, ); let mut tasks = vec![task]; dialog.set_choices(vec![ DialogChoice::ComboBox { id: "example-combobox".into(), label: "Combobox".into(), options: vec![ DialogChoiceOption { id: "foo".into(), label: "foo".into(), }, DialogChoiceOption { id: "bar".into(), label: "bar".into(), }, ], selected: Some(0), }, DialogChoice::CheckBox { id: "example-checkbox".into(), label: "Checkbox".into(), value: false, }, ]); tasks.push(dialog.set_filters( vec![DialogFilter { label: "Any file".into(), patterns: vec![DialogFilterPattern::Glob("*".into())], }], Some(0), )); self.dialog_opt = Some(dialog); return Task::batch(tasks); } } Message::DialogOpenImages => { if self.dialog_opt.is_none() { let (mut dialog, task) = Dialog::new( DialogSettings::new().kind(DialogKind::OpenFile), Message::DialogMessage, Message::DialogResult, ); let mut tasks = vec![task]; tasks.push(dialog.set_filters( vec![ DialogFilter { label: "Images".into(), patterns: vec![DialogFilterPattern::Mime("image/*".into())], }, DialogFilter { label: "Any file".into(), patterns: vec![DialogFilterPattern::Glob("*".into())], }, ], Some(0), )); self.dialog_opt = Some(dialog); return Task::batch(tasks); } } Message::DialogResult(result) => { self.dialog_opt = None; self.result_opt = Some(result); } } Task::none() } fn view_window(&self, window_id: window::Id) -> Element<'_, Message> { match &self.dialog_opt { Some(dialog) => dialog.view(window_id), None => widget::text::body("No dialog").into(), } } fn view(&self) -> Element<'_, Message> { 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() { button = button.on_press(Message::DialogOpen(DialogKind::OpenFile)); } column = column.push(button); } { let mut button = widget::button::standard("Open Image"); if self.dialog_opt.is_none() { button = button.on_press(Message::DialogOpenImages); } column = column.push(button); } { let mut button = widget::button::standard("Open Multiple Files"); if self.dialog_opt.is_none() { button = button.on_press(Message::DialogOpen(DialogKind::OpenMultipleFiles)); } column = column.push(button); } { let mut button = widget::button::standard("Open Folder"); if self.dialog_opt.is_none() { button = button.on_press(Message::DialogOpen(DialogKind::OpenFolder)); } column = column.push(button); } { let mut button = widget::button::standard("Open Multiple Folders"); if self.dialog_opt.is_none() { button = button.on_press(Message::DialogOpen(DialogKind::OpenMultipleFolders)); } column = column.push(button); } { let mut button = widget::button::standard("Save File"); if self.dialog_opt.is_none() { button = button.on_press(Message::DialogOpen(DialogKind::SaveFile { filename: "test".to_string(), })); } column = column.push(button); } if let Some(result) = &self.result_opt { match result { DialogResult::Cancel => { column = column.push(widget::text::body("Cancel")); } DialogResult::Open(paths) => { for path in paths.iter() { column = column.push(widget::text::body(format!("{}", path.display()))); } } } } column.into() } fn subscription(&self) -> Subscription { match &self.dialog_opt { Some(dialog) => dialog.subscription(), None => Subscription::none(), } } } ================================================ FILE: examples/gio-list.rs ================================================ use gio::prelude::*; use std::env; fn main() { let uri = env::args().nth(1).expect("no uri provided"); let file = gio::File::for_uri(&uri); for entry_res in file .enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) .unwrap() { let entry = entry_res.unwrap(); println!("{:?}", entry.display_name()); for attribute in entry.list_attributes(None) { println!( " {:?}: {:?}", attribute, entry.attribute_as_string(&attribute) ); } //TODO: what is the best way to resolve shortcuts? let child = if let Some(target_uri) = entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI) { gio::File::for_uri(&target_uri) } else { file.child(entry.name()) }; println!("{:?}", child.uri()); } } ================================================ FILE: examples/gio-mount.rs ================================================ use gio::prelude::*; use std::env; fn main() { let uri = env::args().nth(1).expect("no uri provided"); let context = glib::MainContext::new(); context.block_on(async { let mount_op = gio::MountOperation::new(); mount_op.connect_ask_password(|mount_op, message, default_user, default_domain, flags| { println!( "{}, {}, {}, {:?}", message, default_user, default_domain, flags ); mount_op.set_anonymous(true); mount_op.reply(gio::MountOperationResult::Handled); }); let file = gio::File::for_uri(&uri); let res = file .mount_enclosing_volume_future(gio::MountMountFlags::empty(), Some(&mount_op)) .await; println!("{:?}", res); }); } ================================================ FILE: examples/gvfs.rs ================================================ use gio::prelude::*; fn main() { let monitor = gio::VolumeMonitor::get(); for drive in monitor.connected_drives() { println!("Drive: {}", drive.name()); for id in drive.enumerate_identifiers() { println!(" ID: {}={:?}", id, drive.identifier(&id)); } for volume in drive.volumes() { println!(" Volume: {}", volume.name()); println!(" UUID: {:?}", volume.uuid()); for id in volume.enumerate_identifiers() { println!(" ID: {}={:?}", id, volume.identifier(&id)); } if let Some(mount) = volume.get_mount() { println!(" Mount: {}", mount.name()); println!(" UUID: {:?}", mount.uuid()); } } } for mount in monitor.mounts() { println!("Mount: {}", mount.name()); println!(" UUID: {:?}", mount.uuid()); } for volume in monitor.volumes() { println!("Volume: {}", volume.name()); println!(" UUID: {:?}", volume.uuid()); for id in volume.enumerate_identifiers() { println!(" ID: {}={:?}", id, volume.identifier(&id)); } if let Some(mount) = volume.get_mount() { println!(" Mount: {}", mount.name()); println!(" UUID: {:?}", mount.uuid()); } } } ================================================ FILE: i18n/af/cosmic_files.ftl ================================================ support = Ondersteuning cancel = Kanselleer settings = Instellings theme = Tema light = Lig dark = Donker cosmic-files = COSMIC Leêrs file = Lêer open-file = Oop 'n lêer connect = Konnekteer password = Wagwoord name = Naam empty-folder = Leë vouer empty-folder-hidden = Leë vouer (met versteekte items) no-results = Geen resultate gevind nie filesystem = Lêerstelsel home = Tuis networks = Netwerke notification-in-progress = Lêerbewerkings is aan die gang. trash = Asblik repository = Kodebank open-folder = Oop 'n vouer match-desktop = Stelselstandaard username = Gebruikersnaam appearance = Vertoon delete = Skrap new-tab = Nuwe oortjie close-tab = Maak oortjie toe quit = Sluit edit = Redigeer copy = Kopieer paste = Plak select-all = Selekteer alles replace = Vervang new-window = Nuwe venster save = Stoor undo = Ontdoen cut = Knip view = Beeld default-size = Standaardgrootte zoom-out = Zoem uit zoom-in = Zoem in menu-settings = Instellings… ================================================ FILE: i18n/ar/cosmic_files.ftl ================================================ cosmic-files = ملفات COSMIC comment = مدير ملفات لسطح مكتب COSMIC keywords = مجلد;ملف;مدير; empty-folder = مجلد فارغ empty-folder-hidden = مجلد فارغ (يحتوي على عناصر مخفية) filesystem = نظام الملفات home = منزل trash = المهملات recents = الأحدث # List view name = الاسم modified = مُعدل size = الحجم # Dialogs ## Empty Trash Dialog empty-trash = أفرغ المهملات empty-trash-warning = سيتم حذف العناصر الموجودة في مجلد المهملات نهائيًا ## New File/Folder Dialog create-new-file = أنشئ ملف جديد create-new-folder = أنشئ مجلَّد جديد file-name = اسم الملف folder-name = اسم المجلَّد file-already-exists = يوجد ملف بهذا الاسم بالفعل folder-already-exists = يوجد مجلد بهذا الاسم بالفعل name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية name-invalid = لا يمكن أن يكون الاسم «{ $filename }» name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات مائلة (/) ## Open/Save Dialog cancel = ألغِ open = افتح open-file = افتح ملف open-folder = افتح مجلَّد open-in-new-tab = افتح في لسان جديد open-in-new-window = افتح في نافذة جديدة open-multiple-files = افتح عدة ملفات open-multiple-folders = افتح عدة مجلّدات save = احفظ save-file = احفظ الملف ## Rename Dialog rename-file = غيِّر اسم الملف rename-folder = غيِّر اسم المجلّد ## Replace Dialog replace = استبدل replace-title = «{ $filename }» موجود بالفعل في هذا المكان replace-warning = أتريد استبداله بالملف الذي تحفظه؟ سيكتب هذا فوق محتوى الملف. # Context Pages ## About ## Operations pending = قيد الانتظار failed = فشِل complete = اكتمل copy_noun = نسخ ## Open with menu-open-with = افتح ب‍استخدام... default-app = { $name } (المبدئي) ## Properties ## Settings settings = الإعدادات ### Appearance appearance = المظهر theme = النسق match-desktop = طابق سطح المكتب dark = داكن light = فاتح # Context menu add-to-sidebar = أضِف إلى الشريط الجانبي new-file = ملف جديد... new-folder = مجلد جديد... open-in-terminal = افتح في الطرفية move-to-trash = انقل إلى المهملات restore-from-trash = استعِد من المهملات remove-from-sidebar = أزِل من الشريط الجانبي sort-by-name = رتّب حسب الاسم sort-by-modified = رتّب حسب التعديل sort-by-size = رتّب حسب الحجم # Menu ## File file = ملف new-tab = لسان جديد new-window = نافذة جديدة rename = غيِّر الاسم... close-tab = أغلق اللسان quit = أنهِ ## Edit edit = عدِّل cut = قصّ copy = انسخ paste = ألصِق select-all = حدِّد الكل ## View zoom-in = قرِّب default-size = الحجم المبدئي zoom-out = بعِّد view = عرض grid-view = عرض الشبكة list-view = عرض اللائحة show-hidden-files = أظهر الملفات المخفية list-directories-first = اسرد المجلدات اولاً menu-settings = الإعدادات... menu-about = عن مدير ملفات COSMIC... dismiss = أهمِل الرسالة no-results = لم يُعثر على نتائج networks = الشبكات notification-in-progress = عمليات الملفات جارية undo = تراجع today = اليوم desktop-view-options = خيارات عرض سطح المكتب... show-on-desktop = أظهِر على سطح المكتب desktop-folder-content = محتوى مجلد سطح المكتب mounted-drives = محركات الأقراص المثبتة trash-folder-icon = أيقونة مجلد المهملات icon-size-and-spacing = حجم الأيقونة والتباعد icon-size = حجم الأيقونة grid-spacing = تباعد شبكي trashed-on = مهمل details = التفاصيل pause = ألبث resume = استئناف create-archive = أنشئ أرشيف extract-password-required = كلمة السر مطلوبة extract-to = استخرِج إلى... extract-to-title = استخرِج إلى مجلّد mount-error = تعذر الوصول إلى القرص create = أنشئ open-item-location = افتح مكان العنصر set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟ favorite-path-error-description = تعذر فتح «{ $path }» قد لا يكون «{ $path }» موجودًا أو قد لا يكون لديك إذن لفتحه. أترغب في إزالته من الشريط الجانبي؟ open-with-title = كيف تريد فتح «{ $name }»؟ read-execute = قراءة وتنفيذ read-write = قراءة وكتابة read-write-execute = قراءة وكتابة وتنفيذ favorite-path-error = خطأ في فتح المجلّد remove = أزِل keep = أبقِ repository = المستودع support = الدعم add-network-drive = أضِف قرص شبكة connect = اتصل connect-anonymously = اتصل بمجهولية connecting = يتصل... domain = النطاق enter-server-address = أدخل عنوان الخادم 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:// network-drive-error = تعذر الوصول القرص الشبكي password = كلمة السر remember-password = تذكر كلمة السر try-again = حاول مجددًا username = اسم المستخدم cancelled = أُلغِيَ edit-history = عدِّل التأريخ history = التأريخ no-history = لا توجد عناصر في التأريخ. progress = { $percent }٪ progress-cancelled = { $percent }٪، أُلغِيَ progress-failed = { $percent }٪، فشل progress-paused = { $percent }٪، أُلبِث creating = ينشئ «{ $name }» في «{ $parent }» created = أُنشئ «{ $name }» في «{ $parent }» emptying-trash = جارِ تفريغ { trash } ({ $progress })... emptied-trash = أُفرِغت { trash } setting-executable-and-launching = يعيِّن «{ $name }» كقابل للتنفيذ ويُشغِّل set-executable-and-launched = عيِّن «{ $name }» كقابل للتنفيذ وشُغَّل setting-permissions = يعيِّن الأذونات لـ«{ $name }» إلى «{ $mode }» set-permissions = عيِّن أذونات «{ $name }» إلى «{ $mode }» renaming = تُعاد تسمية «{ $from }» إلى «{ $to }» renamed = غُيِّر اسم «{ $from }» إلى «{ $to }» unknown-folder = مجلد مجهول show-details = أظهِر التفاصيل type = النوع: { $mime } items = العناصر: { $items } item-size = الحجم: { $size } item-created = أُنشئ في: { $created } item-modified = عُدّل في: { $modified } item-accessed = آخر وصول: { $accessed } calculating = يحسب... single-click = نقرة واحدة للفتح type-to-search = اكتب للبحث type-to-search-recursive = يبحث في المجلّد الحالي وجميع المجلّدات الفرعية type-to-search-enter-path = يدخل المسار إلي المجلّد أو الملف compress = اضغط... delete-permanently = احذف نهائيًا eject = أخرِج extract-here = استخرِج sort-by-trashed = رتّب حسب وقت الحذف remove-from-recents = أزِل من الحديثة change-wallpaper = غيِّر خلفية الشاشة... desktop-appearance = مظهر سطح المكتب... display-settings = إعدادات العرض... reload-folder = أعد تحميل المجلّد gallery-preview = معاينة المعرض sort = رتّب sort-a-z = أ-ي sort-z-a = ي-أ sort-newest-first = الأحدث أولاً sort-oldest-first = الأقدم أولاً sort-smallest-to-largest = من الأصغر إلى الأكبر sort-largest-to-smallest = من الأكبر إلي الأصغر operations-running = { $running } { $running -> [one] عملية *[other] عمليات } قيد التشغيل ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] عملية *[other] عمليات } قيد التشغيل ({ $percent }%)، { $finished } انتهى... browse-store = تصفح { $store } other-apps = تطبيقات أخرى related-apps = تطبيقات ذات صلة selected-items = العناصر { $items } المحدّدة permanently-delete-question = احذف نهائيًا؟ delete = احذف permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء. replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه. original-file = الملف الأصلي replace-with = استبدل بـ apply-to-all = طبِّق على الكلّ keep-both = احتفظ بكليهما skip = تخطَّ set-executable-and-launch = عيِّن كقابل للتنفيذ وشغِّل set-and-launch = عيِّن وشغِّل open-with = افتح بـ owner = المالك group = المجموعة other = أخرى none = لا شيء execute-only = تنفيذ فقط write-only = كتابة فقط write-execute = كتابة وتنفيذ read-only = قراءة فقط compressing = جارِ ضغط { $items } { $items -> [one] عنصر *[other] عناصر } من «{ $from }» إلى «{ $to }» ({ $progress })... compressed = ضُغط { $items } { $items -> [one] عنصر *[other] عنصر } من «{ $from }» إلى «{ $to }» copied = نُسِخ { $items } { $items -> [one] عنصر *[other] عناصر } من «{ $from }» إلى «{ $to }» deleting = يحذف { $items } { $items -> [one] عنصر *[other] عناصر } من { trash } ({ $progress })... deleted = حُذف { $items } { $items -> [one] عنصر *[other] عناصر } من { trash } copying = ينسخ { $items } { $items -> [one] عنصر *[other] عناصر } من «{ $from }» إلى «{ $to }» ({ $progress })... extracting = يستخرِج { $items } { $items -> [one] عنصر *[other] عنصار } من «{ $from }» إلى «{ $to }» ({ $progress })... extracted = استُخرِج { $items } { $items -> [one] عنصر *[other] عناصر } من «{ $from }» إلى «{ $to }» moving = ينقل { $items } { $items -> [one] عنصر *[other] عناصر } من «{ $from }» إلى «{ $to }» ({ $progress })... moved = نُقل { $items } { $items -> [one] item *[other] items } من «{ $from }» إلى «{ $to }» permanently-deleting = يحذف { $items } بشكل نهائي { $items -> [one] عنصر *[other] عناصر } permanently-deleted = حُذف { $items } بشكل نهائي { $items -> [one] عنصر *[other] عناصر } removing-from-recents = يُزيل { $items } { $items -> [one] عنصر *[other] عناصر } من { recents } removed-from-recents = أُزيل { $items } { $items -> [one] عنصر *[other] عناصر } من { recents } restoring = يستعيد { $items } { $items -> [one] عنصر *[other] عناصر } من { trash } ({ $progress })... restored = استُعيد { $items } { $items -> [one] عنصر *[other] عناصر } من { trash } empty-trash-title = أفرغ المهملات؟ type-to-search-select = يختار أول ملف أو مجلد مطابق pasted-image = صورة مُلصقة pasted-text = نص مُلصق pasted-video = فيديو مُلصق copy-to-title = حدِّد وجهة النسخ copy-to-button-label = انسخ move-to-title = حدِّد وجهة النقل move-to-button-label = انقل copy-to = انسخ إلى... move-to = انقل إلى... show-recents = مجلد الحديثة في الشريط الجانبي clear-recents-history = امحُ التأريخ الحديث copy-path = انسخ المسار mixed = مختلط ================================================ FILE: i18n/be/cosmic_files.ftl ================================================ cosmic-files = Файлы COSMIC empty-folder = Пустая папка empty-folder-hidden = Пустая папка (са схаванымі элементамі) no-results = Нічога не знойдзена filesystem = Файлавая сістэма home = Хатняя папка networks = Сеткі notification-in-progress = Ідзе аперацыя з файламі. trash = Сметніца recents = Нядаўняе undo = Адрабіць today = Сёння # Desktop view options desktop-view-options = Параметры выгляду працоўнага стала... show-on-desktop = Паказваць на працоўным стале desktop-folder-content = Змесціва папкі "Працоўны стол" mounted-drives = Змантаваныя дыскі trash-folder-icon = Значок папкі "Сметніца" icon-size-and-spacing = Памер і інтэрвал значкоў icon-size = Памер значкоў grid-spacing = Інтэрвал сеткі # List view name = Назва modified = Зменена trashed-on = Дата выдалення size = Памер # Progress footer details = Падрабязнасці dismiss = Адхіліць паведамленне operations-running = Выконваецца { $running } { $running -> [one] аперацыя [few] аперацыі *[other] аперацый } ({ $percent }%)... operations-running-finished = Выконваецца { $running } { $running -> [one] аперацыя [few] аперацыі *[other] аперацый } ({ $percent }%), { $finished } завершана... pause = Паўза resume = Працягнуць # Dialogs ## Compress Dialog create-archive = Стварыць архіў ## Extract Dialog extract-password-required = Патрабуецца пароль extract-to = Выняць у... extract-to-title = Выняць у папку ## Empty Trash Dialog empty-trash = Ачысціць сметніцу empty-trash-warning = Вы сапраўды хочаце назаўсёды выдаліць усе элементы з сметніцы? ## Mount Error Dialog mount-error = Немагчыма атрымаць доступ да дыска # New File/Folder Dialog create-new-file = Стварыць новы файл create-new-folder = Стварыць новую папку file-name = Назва файла folder-name = Назва папкі file-already-exists = Файл з такой назвай ужо існуе. folder-already-exists = Папка з такой назвай ужо існуе. name-hidden = Назвы, якія пачынаюцца з ".", будуць схаваны. name-invalid = Назва не можа быць "{ $filename }". name-no-slashes = Назва не можа ўтрымліваць касыя рысы. # Open/Save Dialog cancel = Скасаваць create = Стварыць open = Адкрыць open-file = Адкрыць файл open-folder = Адкрыць папку open-in-new-tab = Адкрыць у новай укладцы open-in-new-window = Адкрыць у новым акне open-item-location = Адкрыць месцазнаходжанне элемента open-multiple-files = Адкрыць некалькі файлаў open-multiple-folders = Адкрыць некалькі папак save = Захаваць save-file = Захаваць файл ## Open With Dialog open-with-title = Як вы хочаце адкрыць "{ $name }"? browse-store = Прагляд { $store } other-apps = Іншыя праграмы related-apps = Звязаныя праграмы ## Permanently delete Dialog selected-items = выбрана { $items } элементаў permanently-delete-question = Выдаліць назаўжды delete = Выдаліць permanently-delete-warning = Вы ўпэўненыя, што хочаце назаўжды выдаліць { $target }? Гэта дзеянне немагчыма адмяніць. # Rename Dialog rename-file = Перайменаваць файл rename-folder = Перайменаваць папку # Replace Dialog replace = Замяніць replace-title = { $filename } ужо існуе ў гэтым месцы. replace-warning = Вы сапраўды хочаце замяніць яго на той, які вы захоўваеце? Пры замене яго змесціва будзе перапісана. replace-warning-operation = Вы хочаце замяніць яго? Пры замене яго змесціва будзе перазапісана. original-file = Зыходны файл replace-with = Замяніць на apply-to-all = Прымяніць да ўсіх keep-both = Захаваць абодва skip = Прапусціць ## Set as Executable and Launch Dialog set-executable-and-launch = Зрабіць выканальным і запусціць set-executable-and-launch-description = Вы хочаце зрабіць "{ $name }" выканальным і запусціць? set-and-launch = Задаць і запусціць ## Metadata Dialog open-with = Адкрыць праз owner = Уладальнік group = Група other = Іншыя ### Mode 0 none = Няма ### Mode 1 (unusual) execute-only = Толькі выкананне ### Mode 2 (unusual) write-only = Толькі запіс ### Mode 3 (unusual) write-execute = Запіс і выкананне ### Mode 4 read-only = Толькі чытанне ### Mode 5 read-execute = Чытанне і выкананне ### Mode 6 read-write = Чытанне і запіс ### Mode 7 read-write-execute = Чытанне, запіс і выкананне ## Favorite Path Error Dialog favorite-path-error = Памылка адкрыцця каталога favorite-path-error-description = Немагчыма адкрыць "{ $path }". Магчыма, ён не існуе ці ў вас няма дазволу на яго адкрыццё. Ці хочаце вы выдаліць яго з бакавой панэлі? remove = Выдаліць keep = Захаваць # Context Pages ## About ## Add Network Drive add-network-drive = Дадаць сеткавы дыск connect = Падключыцца connect-anonymously = Падлучыць ананімна connecting = Падлучэнне... domain = Дамен enter-server-address = Увядзіце адрас серверу network-drive-description = Адрасы сервераў ўключаюць у сябе прэфікс пратаколу і адрас. Прыклад: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns 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:// network-drive-error = Немагчыма атрымаць доступ да сеткавага дыска password = Пароль remember-password = Запомніць пароль try-again = Паўтарыць спробу username = Імя карыстальніка ## Operations cancelled = Скасавана edit-history = Гісторыя рэдагавання history = Гісторыя no-history = У гісторыі няма запісаў. pending = У чаканні progress = { $percent }% progress-cancelled = { $percent }%, скасавана progress-paused = { $percent }%, прыпынена failed = Няўдала complete = Завершана compressing = Сцісканне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з «{ $from }» у «{ $to }» ({ $progress })... compressed = Сціснута { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { $from } у { $to } copy_noun = Капіяваць creating = Стварэнне { $name } у { $parent } created = Створана { $name } у { $parent } copying = Капіяванне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з «{ $from }» у «{ $to }» ({ $progress })... copied = Скапіявана { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { $from } у { $to } deleting = Выдаленне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } са { trash } ({ $progress })... deleted = Выдалена { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } са { trash } emptying-trash = Ачыстка { trash } ({ $progress })… emptied-trash = Ачышчана { trash } extracting = Выманне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з «{ $from }» у «{ $to }» ({ $progress })... extracted = Вынята { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { $from } у { $to } setting-executable-and-launching = Робім "{ $name }" выканальным і запускаем set-executable-and-launched = "{ $name }" зроблены выканальным і запушчаны setting-permissions = Усталёўваем дазволы для "{ $name }" на { $mode } set-permissions = Дазволы для "{ $name }" усталяваны на { $mode } moving = Перамяшчэнне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з «{ $from }» у «{ $to }» ({ $progress })... moved = Перанесена { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { $from } у { $to } permanently-deleting = Назаўсёды выдаляем { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } permanently-deleted = Назаўсёды выдалена { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } renaming = Перайменаванне { $from } у { $to } renamed = Перайменавана { $from } у { $to } restoring = Аднаўленне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з { trash } ({ $progress })... restored = Адноўлена { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { trash } unknown-folder = невядомая папка ## Open with menu-open-with = Адкрыць праз... default-app = { $name } (па змаўчанні) ## Show details show-details = Паказаць дэталі type = Тып: { $mime } items = Элементаў: { $items } item-size = Памер: { $size } item-created = Створана: { $created } item-modified = Зменена: { $modified } item-accessed = Апошні доступ: { $accessed } calculating = Вылічэнне... ## Settings settings = Налады single-click = Адзін клік каб адкрыць ### Appearance appearance = Знешні выгляд theme = Тэма match-desktop = Як у сістэме dark = Цёмная light = Светлая ### Type to Search type-to-search = Увядзіце для пошуку type-to-search-recursive = Пошук у бягучай і ўкладзеных папках type-to-search-enter-path = Увод шляху да каталога або файла # Context menu add-to-sidebar = Дадаць на бакавую панэль compress = Сціснуць delete-permanently = Выдаліць назаўжды eject = Выняць extract-here = Выняць new-file = Новы файл... new-folder = Новая папка... open-in-terminal = Адкрыць у кансолі move-to-trash = Перамясціць у сметніцу restore-from-trash = Аднавіць са сметніцы remove-from-sidebar = Выдаліць з бакавой панэлі sort-by-name = Сартаваць па назве sort-by-modified = Сартаваць па даце змянення sort-by-size = Сартаваць па памеры sort-by-trashed = Сартаваць па часе выдалення ## Desktop change-wallpaper = Змяніць шпалеры... desktop-appearance = Выгляд працоўнага стала... display-settings = Налады дысплэя... # Menu ## File file = Файл new-tab = Новая ўкладка new-window = Новае акно reload-folder = Абнавіць папку rename = Перайменаваць... close-tab = Закрыць укладку quit = Выйсці ## Edit edit = Рэдагаваць cut = Выразаць copy = Скапіяваць paste = Уставіць select-all = Вылучыць усё ## View zoom-in = Павялічыць default-size = Памер па змаўчанні zoom-out = Паменшыць view = Выгляд grid-view = Рэжым сеткі list-view = Рэжым спіса show-hidden-files = Паказваць схаваныя файлы list-directories-first = Размяшчаць папкі перад файламі gallery-preview = Папярэдні прагляд menu-settings = Налады... menu-about = Пра Файлы COSMIC... ## Sort sort = Сартаванне sort-a-z = А-Я sort-z-a = Я-А sort-newest-first = Спачатку новыя sort-oldest-first = Спачатку старыя sort-smallest-to-largest = Ад меншага да найбольшага sort-largest-to-smallest = Ад вялікага да найменшага repository = Рэпазіторый support = Падтрымка progress-failed = { $percent }%, няўдала removing-from-recents = Выдаленне { $items } { $items -> [one] элемента [few] элементы *[other] элементаў } з { recents } removed-from-recents = Выдалена { $items } { $items -> [one] элемент [few] элементы *[other] элементаў } з { recents } remove-from-recents = Выдаліць з нядаўніх ================================================ FILE: i18n/bg/cosmic_files.ftl ================================================ cosmic-files = Файлове на COSMIC empty-folder = Празна папка empty-folder-hidden = Празна папка (съдържа скрити елементи) no-results = Няма намерени резултати filesystem = Файлова система home = Домашна папка networks = Мрежи notification-in-progress = Файлови операции са в процес на изпълнение. trash = Кошче recents = Скоро ползвани undo = Отменяне today = Днес # Desktop view options desktop-view-options = Опции за изглед на работния плот... show-on-desktop = Покажи на работния плот desktop-folder-content = Съдържание на папката на работния плот mounted-drives = Монтирани устройства trash-folder-icon = Иконка на кошчето icon-size-and-spacing = Размер и разстояние между иконките icon-size = Размер grid-spacing = Разстояние # List view name = Име modified = Променян trashed-on = Изтрит size = Размер # Progress footer details = Подробности dismiss = Отмяна на съобщението operations-running = { $running } { $running -> [one] операция се изпълнява *[other] операции се изпълняват } ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] операция се изпълнява *[other] операции се изпълняват } ({ $percent }%), { $finished } завършиха... pause = Пауза resume = Продължаване # Dialogs ## Compress Dialog create-archive = Създаване на архив ## Extract Dialog extract-password-required = Необходима е парола extract-to = Разархивиране в... extract-to-title = Разархивиране в папка ## Empty Trash Dialog empty-trash = Изпразване на кошчето empty-trash-warning = Сигурни ли сте, че искате да изтриете завинаги всички елементи в кошчето? ## Mount Error Dialog mount-error = Устройството не може да бъде достъпно ## New File/Folder Dialog create-new-file = Създаване на нов файл create-new-folder = Създаване на нова папка file-name = Име на файла folder-name = Име на папката file-already-exists = Вече съществува файл с това име. folder-already-exists = Вече съществува папка с това име. name-hidden = Имената, започващи с „.“, ще бъдат скрити. name-invalid = Името не може да бъде „{ $filename }“. name-no-slashes = Името не може да съдържа наклонени черти. ## Open/Save Dialog cancel = Отказване create = Създаване open = Отваряне open-file = Отваряне на файл open-folder = Отваряне на папка open-in-new-tab = Отваряне в нов раздел open-in-new-window = Отваряне в нов прозорец open-item-location = Отваряне на местоположението на обекта open-multiple-files = Отваряне на няколко файла open-multiple-folders = Отваряне на няколко папки save = Запазване save-file = Запазване на файла ## Open With Dialog open-with-title = Как искате да отворите „{ $name }“? browse-store = Разгледайте { $store } other-apps = Други програми related-apps = Свързани програми ## Permanently delete Dialog selected-items = избраните { $items } елемента permanently-delete-question = Изтриване завинаги delete = Изтриване permanently-delete-warning = Сигурни ли сте, че искате да изтриете завинаги { $target }? Това действие не може да бъде върнато. ## Rename Dialog rename-file = Преименуване на файла rename-folder = Преименуване на папката ## Replace Dialog replace = Заменяне replace-title = „{ $filename }“ вече съществува на това местоположение. replace-warning = Искате ли да го замените с този, който запазвате? Ако го замените, ще презапишете съдържанието му. replace-warning-operation = Искате ли да го замените? Ако го замените, ще презапишете съдържанието му. original-file = Съществуващ файл replace-with = Замяна с apply-to-all = Прилагане за всички keep-both = Запазване на и двата skip = Пропускане ## Set as Executable and Launch Dialog set-executable-and-launch = Задаване като изпълним и стартиране set-executable-and-launch-description = Искате ли да зададете „{ $name }“ като изпълним и да го стартирате? set-and-launch = Задаване и стартиране ## Metadata Dialog open-with = Отваряне с owner = Собственик group = Група other = Друго ### Mode 0 none = Без ### Mode 1 (unusual) execute-only = Само за изпълняване ### Mode 2 (unusual) write-only = Само за записване ### Mode 3 (unusual) write-execute = Записване и изпълняване ### Mode 4 read-only = Само за четене ### Mode 5 read-execute = Четене и изпълняване ### Mode 6 read-write = Четене и записване ### Mode 7 read-write-execute = Четене, записване и изпълняване ## Favorite Path Error Dialog favorite-path-error = Грешка при отваряне на папката favorite-path-error-description = Местоположението „{ $path }“ не може да бъде отворено. Възможно е да не съществува или да нямате права да го отворите. Искате ли да го премахнете от страничната лента? remove = Премахване keep = Запазване # Context Pages ## About repository = Хранилище support = Поддръжка ## Add Network Drive add-network-drive = Добавяне на мрежово устройство connect = Свързване connect-anonymously = Свързване анонимно connecting = Свързване... domain = Домейн enter-server-address = Въведете адрес на сървър network-drive-description = Адресите на сървърите включват представка на протокола и адрес. Примери: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Налични протоколи,Представка AppleTalk,afp:// Протокол за пренос на файлове,ftp:// или ftps:// Мрежова файлова система,nfs:// Server Message Block,smb:// Пренос на файлове по SSH,sftp:// или ssh:// WebDav,dav:// или davs:// network-drive-error = Мрежовото устройство не може да бъде достъпно password = Парола remember-password = Запомняне на паролата try-again = Опитайте отново username = Потребителско име ## Operations cancelled = Отменена edit-history = Редактиране на историята history = История no-history = Няма елементи в историята. pending = Чакащи progress = { $percent }% progress-cancelled = { $percent }%, отменена progress-failed = { $percent }%, неуспешно progress-paused = { $percent }%, на пауза failed = Неуспешна complete = Завършена compressing = Компресиране на { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ ({ $progress })... compressed = Компресирано е { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ copy_noun = Копиране creating = Създаване на „{ $name }“ в „{ $parent }“ created = „{ $name }“ е създадено в „{ $parent }“ copying = Копиране на { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ ({ $progress })... copied = Копирано е { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ deleting = Изтриване на { $items } { $items -> [one] елемент *[other] елемента } от { trash } ({ $progress })... deleted = Изтрито е { $items } { $items -> [one] елемент *[other] елемента } от { trash } emptying-trash = Изпразване на { trash } ({ $progress })... emptied-trash = { trash } е изпразнено extracting = Извличане на { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ ({ $progress })... extracted = Извлечено е { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ setting-executable-and-launching = Задаване на „{ $name }“ като изпълним и стартиране set-executable-and-launched = „{ $name }“ е зададен като изпълним и е стартиран setting-permissions = Задаване на правата за { $name }" на { $mode } set-permissions = Правата за "{ $name }" бяха зададени на { $mode } moving = Преместване на { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ ({ $progress })... moved = Преместено е { $items } { $items -> [one] елемент *[other] елемента } от „{ $from }“ в „{ $to }“ permanently-deleting = Изтриване завинаги на { $items } { $items -> [one] елемент *[other] елемента } permanently-deleted = Изтрито е завинаги { $items } { $items -> [one] елемент *[other] елемента } removing-from-recents = Премахване на { $items } { $items -> [one] елемент *[other] елемента } от { recents } removed-from-recents = Премахнато е { $items } { $items -> [one] елемент *[other] елемента } от { recents } renaming = Преименуване на „{ $from }“ на „{ $to }“ renamed = „{ $from }“ е преименувано на „{ $to }“ restoring = Възстановяване на { $items } { $items -> [one] елемент *[other] елемента } от { trash } ({ $progress })... restored = Възстановено е { $items } { $items -> [one] елемент *[other] елемента } от { trash } unknown-folder = неизвестна папка ## Open with menu-open-with = Отваряне с... default-app = { $name } (стандартно) ## Show details show-details = Показване на подробностите type = Вид: { $mime } items = Елементи: { $items } item-size = Размер: { $size } item-created = Създаден: { $created } item-modified = Променян: { $modified } item-accessed = Достъпен: { $accessed } calculating = Изчисляване... ## Settings settings = Настройки single-click = Отваряне с едно натискане ### Appearance appearance = Външен вид theme = Тема match-desktop = Системна тема dark = Тъмна тема light = Светла тема ### Type to Search type-to-search = Въведете текст за търсене type-to-search-recursive = Търсене в текущата папка и всички подпапки type-to-search-enter-path = Въвежда пътя до папката или файла # Context menu add-to-sidebar = Добавяне към страничната лента compress = Компресиране delete-permanently = Изтриване завинаги eject = Изваждане extract-here = Разархивиране new-file = Нов файл... new-folder = Нова папка... open-in-terminal = Отваряне в терминала move-to-trash = Преместване в кошчето restore-from-trash = Възстановяване от кошчето remove-from-sidebar = Премахване от стр. лента sort-by-name = Подреждане по име sort-by-modified = Подреждане по дата на променяне sort-by-size = Подреждане по размер sort-by-trashed = Подреждане по дата на изтриване remove-from-recents = Премахване от скорошни ## Desktop change-wallpaper = Променяне на фона... desktop-appearance = Външен вид на работния плот... display-settings = Настройки на екрана... # Menu ## File file = Файл new-tab = Нов подпрозорец new-window = Нов прозорец reload-folder = Презареждане на папката rename = Преименуване... close-tab = Затваряне на подпрозореца quit = Спиране на програмата ## Edit edit = Редактиране cut = Отрязване copy = Копиране paste = Поставяне select-all = Избор на всички ## View zoom-in = Увеличаване default-size = Стандартен размер zoom-out = Намаляване view = Изглед grid-view = Изглед като решетка list-view = Изглед като списък show-hidden-files = Показване на скритите файлове list-directories-first = Изброяване първо на папките gallery-preview = Изглед като галерия menu-settings = Настройки... menu-about = Относно „Файлове на COSMIC“... ## Sort sort = Подреждане sort-a-z = А→Я sort-z-a = Я→А sort-newest-first = Най-новите първи sort-oldest-first = Най-старите първи sort-smallest-to-largest = Най-малките до най-големите sort-largest-to-smallest = Най-големите до най-малките ================================================ FILE: i18n/bn/cosmic_files.ftl ================================================ ================================================ FILE: i18n/ca/cosmic_files.ftl ================================================ cosmic-files = Fitxers del COSMIC empty-folder = Carpeta buida empty-folder-hidden = Carpeta buida (té elements ocults) no-results = No s'ha trobat cap resultat filesystem = Sistema de fitxers home = Inici networks = Xarxes notification-in-progress = Hi ha operacions amb fitxers en curs. trash = Paperera recents = Recents undo = Desfés today = Avui # Desktop view options desktop-view-options = Opcions de visualització de l'escriptori show-on-desktop = Mostra a l'escriptori desktop-folder-content = Contingut de la carpeta de l'escriptori mounted-drives = Unitats muntades trash-folder-icon = Icona de la paperera icon-size-and-spacing = Mida i espaiat de les icones icon-size = Mida de les icones grid-spacing = Espaiat de la quadrícula # List view name = Nom modified = Modificat trashed-on = Mogut a la paperera size = Mida # Progress footer details = Detalls dismiss = Descarta el missatge operations-running = { $running } { $running -> [one] operacion *[other] operacions } en curs ({ $percent }%)... operations-running-finished = { $running }{ $running -> [one] operacion *[other] operacions } en curs ({ $percent }%), { $finished } acabat... pause = Pausa resume = Reprèn # Dialogs ## Compress Dialog create-archive = Crea un arxiu ## Extract Dialog extract-password-required = Cal una contrasenya ## Empty Trash Dialog empty-trash = Buida la paperera empty-trash-warning = Voleu suprimir permanentment tots els fitxers de la paperera? ## Mount Error Dialog mount-error = No es pot accedir a la unitat ## New File/Folder Dialog create-new-file = Crea un nou fitxer create-new-folder = Crea una nova carpeta file-name = Nom del fixer folder-name = Nom de la carpeta file-already-exists = Ja existeix un fitxer amb aquest nom. folder-already-exists = Ja existeix una carpeta amb aquest nom. name-hidden = Els noms que comencin amb "." seran ocults. name-invalid = El nom no pot ser "{ $filename }". name-no-slashes = El nom no pot contenir barres. ## Open/Save Dialog cancel = Cancel·la create = Crea open = Obre open-file = Obre el fixer open-folder = Obre la carpeta open-in-new-tab = Obre en una pestanya nova open-in-new-window = Obre en una finestra nova open-item-location = Obre la ubicació del fitxer open-multiple-files = Obre múltiples fitxers open-multiple-folders = Obre múltiples carpetes save = Desa save-file = Desa el fitxer ## Open With Dialog open-with-title = Com voleu obrir "{ $name }"? browse-store = Navega { $store } ## Rename Dialog rename-file = Canvia el nom del fitxer rename-folder = Canvia el nom de la carpeta ## Replace Dialog replace = Reemplaça replace-title = Ja existeix "{ $filename }" en aquesta ubicació. replace-warning = Voleu reemplaçar-lo pel fitxer que esteu desant? El seu contingut serà sobreescrit. replace-warning-operation = Voleu reemplaçar-lo? El seu contingut serà sobreescrit. original-file = Fitxer original replace-with = Reemplaça amb apply-to-all = Aplica-ho a tot keep-both = Mantén els dos skip = Omet ## Set as Executable and Launch Dialog set-executable-and-launch = Defineix com a executable i executa set-executable-and-launch-description = Voleu definir "{ $name }" com a executable i executar-lo? set-and-launch = Defineix i executa ## Metadata Dialog open-with = Obre amb owner = Propietari group = Grup other = Altre ### Mode 0 none = Cap ### Mode 1 (unusual) execute-only = Només executar ### Mode 2 (unusual) write-only = Només escriure ### Mode 3 (unusual) write-execute = Escriure i executar ### Mode 4 read-only = Només llegir ### Mode 5 read-execute = Llegir i executar ### Mode 6 read-write = Llegir i escriure ### Mode 7 read-write-execute = Llegir, escriure i executar # Context Pages ## About ## Add Network Drive add-network-drive = Afegeix una unitat de la xarxa connect = Connecta connect-anonymously = Connecta anònimament connecting = Connectant... domain = Domini enter-server-address = Introduïu l'adreça del servidor network-drive-description = Les adreces de servidor estan formades per un prefix del protocol i una adreça. Examples: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocols disponibles,Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDav,dav:// or davs:// network-drive-error = No s'ha pogut accedir a la unitat de la xarxa password = Contrasenya remember-password = Recorda la contrasenya try-again = Torna-ho a provar username = Nom d'usuari ## Operations cancelled = Cancel·lat edit-history = Edita l'historial history = Historial no-history = Historial buit. pending = Pendent progress = { $percent }% progress-cancelled = { $percent }%, cancel·lat progress-paused = { $percent }%, en pausa failed = Ha fallat complete = Complet compressing = { $items -> [one] S'està comprimint { $items } element *[other] S'estan comprimint { $items } elements } de "{ $from }" a "{ $to }" ({ $progress })... compressed = { $items -> [one] S'ha comprimit { $items } element *[other] S'han comprimit { $items } elements } de "{ $from }" a "{ $to }" copy_noun = Copia creating = S'està creant "{ $name }" a "{ $parent }" created = S'ha creat "{ $name }" a "{ $parent }" copying = { $items -> [one] S'està copiant { $items } element *[other] S'estan copiant { $items } elements } de "{ $from }" a "{ $to }" ({ $progress })... copied = { $items -> [one] S'ha copiat { $items } element *[other] S'han copiat { $items } elements } de "{ $from }" a "{ $to }" emptying-trash = S'està buidant { trash } ({ $progress })... emptied-trash = S'ha buidat { trash } extracting = { $items -> [one] S'està extraient { $items } element *[other] S'estan extraient { $items } elements } de "{ $from }" a "{ $to }" ({ $progress })... extracted = { $items -> [one] S'ha extret { $items } element *[other] S'han extret { $items } elements } de "{ $from }" a "{ $to }" setting-executable-and-launching = S'està definint "{ $name }" com a executable i executant set-executable-and-launched = S'ha definit "{ $name }" com a executable i executat moving = { $items -> [one] S'està movent { $items } element *[other] S'estan movent { $items } elements } de "{ $from }" a "{ $to }" ({ $progress })... moved = { $items -> [one] S'ha mogut { $items } element *[other] S'han mogut { $items } elements } de "{ $from }" a "{ $to }" renaming = S'està canviant el nom de "{ $from }" a "{ $to }" renamed = S'ha canviat el nom de "{ $from }" a "{ $to }" restoring = { $items -> [one] S'està restaurant { $items } element *[other] S'estan restaurant { $items } elements } de { trash } ({ $progress })... restored = { $items -> [one] S'ha restaurat { $items } element *[other] S'han restaurat { $items } elements } de { trash } unknown-folder = carpeta desconeguda ## Open with menu-open-with = Obre amb... default-app = { $name } (per defecte) ## Show details show-details = Mostra els detalls type = Tipus: { $mime } items = Elements: { $items } item-size = Mida: { $size } item-created = Creat: { $created } item-modified = Modificat: { $modified } item-accessed = Accedit: { $accessed } calculating = S'està calculant... ## Settings settings = Configuració ### Appearance appearance = Aparença theme = Tema match-desktop = Coincideix amb l'escriptori dark = Fosc light = Clar # Context menu add-to-sidebar = Afegeix a la barra lateral compress = Comprimeix extract-here = Extreu new-file = Nou fitxer... new-folder = Nova carpeta... open-in-terminal = Obre al terminal move-to-trash = Mou a la paperera restore-from-trash = Restaura de la paperera remove-from-sidebar = Elimina de la barra lateral sort-by-name = Ordena per nom sort-by-modified = Ordena per data de modificació sort-by-size = Ordena per mida sort-by-trashed = Ordena per data de supressió ## Desktop change-wallpaper = Canvia el fons de pantalla... desktop-appearance = Aparença de l'escriptori... display-settings = Configuració de visualització... # Menu ## File file = Fitxer new-tab = Pestanya nova new-window = Finestra nova rename = Canvia el nom... close-tab = Tanca la pestanya quit = Surt ## Edit edit = Edita cut = Retalla copy = Copia paste = Enganxa select-all = Selecciona-ho tot ## View zoom-in = Amplia default-size = Mida predeterminada zoom-out = Redueix view = Visualització grid-view = Vista de graella list-view = Vista de llista show-hidden-files = Mostra els fitxers ocults list-directories-first = Mostra els directoris primer gallery-preview = Vista prèvia en galeria menu-settings = Configuració... menu-about = Quant a Fitxers del COSMIC... ## Sort sort = Ordena sort-a-z = A-Z sort-z-a = Z-A 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 ================================================ FILE: i18n/cs/cosmic_files.ftl ================================================ cosmic-files = Soubory COSMIC comment = Správce souborů pro prostředí COSMIC keywords = Složka;Složky;Správce;Manažer;Prohlížeč; empty-folder = Složka je prázdná empty-folder-hidden = Složka je prázdná (obsahuje skryté položky) filesystem = Souborový systém home = Domů recents = Nedávné trash = Koš # List view name = Název modified = Datum změny size = Velikost # Dialogs ## Empty Trash Dialog empty-trash = Vysypat koš empty-trash-warning = Položky v koši budou trvale smazány ## New File/Folder Dialog create-new-file = Vytvořit nový soubor create-new-folder = Vytvořit novou složku file-name = Název souboru folder-name = Název složky file-already-exists = Již existuje soubor s daným názvem folder-already-exists = Již existuje složka s daným názvem name-hidden = Položky s názvem začínajícím tečkou budou skryty name-invalid = Název nemůže být „{ $filename }“ name-no-slashes = Název nesmí obsahovat lomítka ## Open/Save Dialog cancel = Zrušit open = Otevřít open-file = Otevřít soubor open-folder = Otevřít složku open-in-new-tab = Otevřít na nové kartě open-in-new-window = Ovevřít v novém okně open-multiple-files = Otevřít více souborů open-multiple-folders = Otevřít více složek save = Uložit save-file = Uložit soubor ## Rename Dialog rename-file = Přejmenovat soubor rename-folder = Přejmenovat složku ## Replace Dialog replace = Nahradit replace-title = Soubor „{ $filename }“ již na daném místě existuje replace-warning = Chcete nahradit soubor tím, který ukládáte? Nahrazení přepíše veškerý jeho obsah. # Context Pages ## About ## Operations pending = Nevyřízené failed = Neúspěšné complete = Dokončené copy_noun = Kopie ## Open with menu-open-with = Otevřít pomocí... default-app = { $name } (výchozí) ## Properties ## Settings settings = Nastavení ### Appearance appearance = Vzhled theme = Motiv match-desktop = Podle systému dark = Tmavý light = Světlý # Context menu add-to-sidebar = Přidat do postranního panelu new-file = Nový soubor... new-folder = Nová složka... open-in-terminal = Otevřít v terminálu move-to-trash = Přesunout do koše restore-from-trash = Obnovit z koše remove-from-sidebar = Odstranit z postranního panelu sort-by-name = Seřadit podle názvu sort-by-modified = Seřadit podle data změny sort-by-size = Seřadit podle velikosti # Menu ## File file = Soubor new-tab = Nová karta new-window = Nové okno rename = Přejmenovat... close-tab = Zavřít kartu quit = Ukončit ## Edit edit = Úpravy cut = Vyjmout copy = Kopírovat paste = Vložit select-all = Vybrat vše ## View zoom-in = Přiblížit default-size = Výchozí velikost zoom-out = Oddálit view = Zobrazení grid-view = Zobrazit jako mřížku list-view = Zobrazit jako seznam show-hidden-files = Zobrazit skryté soubory list-directories-first = Řadit nejprve složky menu-settings = Nastavení... menu-about = O aplikaci Soubory COSMIC... no-results = Nenalezeny žádné výsledky repository = Repozitář support = Podpora networks = Sítě notification-in-progress = Probíhají operace se soubory undo = Vrátit connect = Připojit today = Dnes desktop-view-options = Možnosti zobrazení plochy... show-on-desktop = Zobrazit na ploše desktop-folder-content = Obsah složky na ploše mounted-drives = Připojené disky trash-folder-icon = Ikona koše icon-size = Velikost ikony password = Heslo remove = Odstranit username = Uživatelské jméno details = Podrobnosti pause = Pozastavit resume = Pokračovat create-archive = Vytvořit archiv extract-password-required = Vyžadováno heslo extract-to = Rozbalit do... extract-to-title = Rozbalit do složky mount-error = Nelze přistoupit k disku create = Vytvořit open-item-location = Otevřít umístění položky open-with-title = Jak chcete otevřít „{ $name }“? browse-store = Procházet { $store } other-apps = Ostatní aplikace related-apps = Související aplikace permanently-delete-question = Trvale smazat? delete = Smazat permanently-delete-warning = Dojde k trvalému smazání { $target }. Tuto akci nelze vrátit. replace-warning-operation = Chcete soubor nahradit? Nahrazení přepíše veškerý jeho obsah. original-file = Původní soubor replace-with = Nahradit za keep-both = Ponechat oba skip = Přeskočit set-executable-and-launch = Povolit spouštění a spustit set-executable-and-launch-description = Chcete povolit spouštění souboru „{ $name }“ a následně ho spustit? set-and-launch = Povolit a spustit open-with = Otevřít pomocí other = Ostatní none = Žádné icon-size-and-spacing = Velikost a rozestupy ikon grid-spacing = Rozestupy mřížky deleting = Mazání { $items } { $items -> [one] položky *[other] položek } z koše ({ $progress })... sort-by-trashed = Seřadit podle času smazání deleted = { $items -> [one] Smazána [few] Smazány *[other] Smazáno } { $items } { $items -> [one] položka [few] položky *[other] položek } z koše emptying-trash = Vysypávání koše ({ $progress })... emptied-trash = Koš vysypán restoring = Obnovování { $items } { $items -> [one] položky *[other] položek } z koše ({ $progress })... restored = { $items -> [one] Obnovena [few] Obnoveny *[other] Obnoveno } { $items } { $items -> [one] položka [few] položky *[other] položek } z koše permanently-deleted = Trvale { $items -> [one] smazána [few] smazány *[other] smazáno } { $items } { $items -> [one] položka [few] položky *[other] položek } delete-permanently = Smazat trvale trashed-on = Smazáno dismiss = Zavřít zprávu operations-running = Běží { $running } { $running -> [one] operace [few] operace *[other] operací } ({ $percent }%)... operations-running-finished = Běží { $running } { $running -> [one] operace [few] operace *[other] operací } ({ $percent }%), { $finished } { $finished -> [one] dokončena... [few] dokončeny... *[other] dokončeno... } apply-to-all = Použít na vše owner = Vlastník group = Skupina execute-only = Pouze spouštění write-only = Pouze zápis write-execute = Zápis a spouštění read-only = Pouze čtení add-network-drive = Přidat síťový disk connect-anonymously = Připojit se anonymně connecting = Připojování... domain = Doména enter-server-address = Zadejte adresu serveru network-drive-description = Adresy serveru obsahují prefix protokolu a adresu. Příklady: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Dostupné protokoly,Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// nebo ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// nebo ssh:// WebDAV,dav:// nebo davs:// network-drive-error = Nelze přistoupit k síťovému disku remember-password = Zapamatovat heslo try-again = Zkusit znovu cancelled = Zrušené edit-history = Historie úprav history = Historie no-history = Žádné položky v historii. progress = { $percent }% progress-cancelled = { $percent }%, zrušeno progress-failed = { $percent }%, selhalo progress-paused = { $percent }%, pozastaveno keep = Ponechat compressing = Balení { $items } { $items -> [one] položky *[other] položek } z „{ $from }“ do „{ $to }“ ({ $progress })... compressed = { $items -> [one] Zabalena [few] Zabaleny *[other] Zabaleno } { $items } { $items -> [one] položka [few] položky *[other] položek } z „{ $from }“ do „{ $to }“ creating = Vytváření položky „{ $name }“ v „{ $parent }“ created = Vytvořena položka „{ $name }“ v „{ $parent }“ copying = Kopírování { $items } { $items -> [one] položky *[other] položek } z „{ $from }“ do „{ $to }“ ({ $progress })... copied = { $items -> [one] Zkopírována [few] Zkopírovány *[other] Zkopírováno } { $items } { $items -> [one] položka [few] položky *[other] položek } z „{ $from }“ do „{ $to }“ extracting = Rozbalování { $items } { $items -> [one] položky *[other] položek } z „{ $from }“ do „{ $to }“ ({ $progress })... favorite-path-error-description = Nelze otevřít „{ $path }“ „{ $path }“ buď neexistuje nebo nemáte dostatečná práva pro otevření Chcete položku odstranit z postranního panelu? selected-items = { $items } vybraných položek read-execute = Čtení a spouštění read-write-execute = Čtení, zápis a spouštění read-write = Čtení a zápis favorite-path-error = Chyba otevírání složky extracted = { $items -> [one] Rozbalena [few] Rozbaleny *[other] Rozbaleno } { $items } { $items -> [one] položka [few] položky *[other] položek } z „{ $from }“ do „{ $to }“ setting-executable-and-launching = Nastavování souboru „{ $name }“ jako spustitelného a spouštění set-executable-and-launched = Soubor „{ $name }“ nastaven jako spustitelný a spuštěn setting-permissions = Nastavování práv položky „{ $name }“ na { $mode } set-permissions = Práva položky „{ $name }“ nastavena na { $mode } moving = Přesouvání { $items } { $items -> [one] položky *[other] položek } z „{ $from }“ do „{ $to }“ ({ $progress })... moved = { $items -> [one] Přesunuta [few] Přesunuty *[other] Přesunuto } { $items } { $items -> [one] položka [few] položky *[other] položek } z „{ $from }“ do „{ $to }“ permanently-deleting = Trvalé mazání { $items } { $items -> [one] položky *[other] položek } removing-from-recents = Odstraňování { $items } { $items -> [one] položky *[other] položek } z { recents } removed-from-recents = { $items -> [one] Odstraněna [few] Odstraněny *[other] Odstraněno } { $items } { $items -> [one] položka [few] položky *[other] položek } z { recents } remove-from-recents = Odstranit z nedávných renaming = Přejmenování „{ $from }“ na „{ $to }“ renamed = Přejmenováno „{ $from }“ na „{ $to }“ unknown-folder = neznámá složka show-details = Zobrazit podrobnosti type = Typ: { $mime } items = Položky: { $items } item-size = Velikost: { $size } item-created = Vytvořeno: { $created } item-modified = Změněno: { $modified } item-accessed = Poslední přístup: { $accessed } calculating = Vypočítávání... single-click = Otevřít jedním kliknutím type-to-search = Vyhledávání psaním type-to-search-recursive = Prohledává aktuální složku a její podsložky type-to-search-enter-path = Zadává cestu ke složce nebo souboru compress = Zabalit... eject = Vysunout extract-here = Rozbalit change-wallpaper = Změnit tapetu... desktop-appearance = Vzhled plochy... display-settings = Nastavení obrazovky... reload-folder = Znovu načíst složku sort-z-a = Z-A sort-newest-first = Nejnovější první sort-oldest-first = Nejstarší první sort-smallest-to-largest = Od nejmenšího po největší 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š? type-to-search-select = Vybere první shodující se soubor nebo složku pasted-image = Vložený obrázek pasted-text = Vložený text pasted-video = Vložené video copy-to-button-label = Kopírovat move-to-button-label = Přesunout copy-to = Kopírovat do... move-to = Přesunout do... copy-to-title = Vyberte cíl kopírování 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é ================================================ FILE: i18n/da/cosmic_files.ftl ================================================ cosmic-files = COSMIC Filer empty-folder = Tom mappe empty-folder-hidden = Tom mappe (har skjulte filer) no-results = Ingen resultater filesystem = Filsystem home = Hjem networks = Netværk notification-in-progress = Filoperationer er igangværende. trash = Skraldespand recents = Seneste undo = Fortryd today = I dag # Desktop view options desktop-view-options = Indstillinger for skrivebordsudseende... show-on-desktop = Vis på Skrivebordet desktop-folder-content = Indhold på Skrivebordet mounted-drives = Monterede drev trash-folder-icon = Skraldespandsikon icon-size-and-spacing = Ikonstørrelse og afstand icon-size = Ikonstørrelse # List view name = Navn modified = Ændret trashed-on = Smidt ud size = Størrelse # Progress footer details = Detaljer dismiss = Afvis besked operations-running = { $running } operationer er i gang ({ $percent }%)... operations-running-finished = { $running } operationer er i gang ({ $percent }%), { $finished } færdiggjort... pause = Pause resume = Fortsæt # Dialogs ## Compress Dialog create-archive = Opret arkiv ## Empty Trash Dialog empty-trash = Tøm skraldespand empty-trash-warning = Er du sikker på du vil slette alle objekter i Skraldespanden permanent? ## Mount Error Dialog mount-error = Kan ikke tilgå drev ## New File/Folder Dialog create-new-file = Opret ny fil create-new-folder = Opret ny mappe file-name = Filnavn folder-name = Mappenavn file-already-exists = En fil med det navn eksisterer allerede. folder-already-exists = En mappe med det navn eksisterer allerede. name-hidden = Navne begyndende med "." vil blive skjult. name-invalid = Navn kan ikke være "{ $filename }". name-no-slashes = Navn kan ikke indeholde skråstreg. ## Open/Save Dialog cancel = Annullér create = Opret open = Åbn open-file = Åbn fil open-folder = Åbn mappe open-in-new-tab = Åbn i ny fane open-in-new-window = Åbn i nyt vindue open-item-location = Åbn placering for objekt open-multiple-files = Åbn flere filer open-multiple-folders = Åbn flere mapper save = Gem save-file = Gem fil ## Open With Dialog open-with-title = Hvordan vil du åbne "{ $name }"? browse-store = Gennemse { $store } ## Rename Dialog rename-file = Omdøb fil rename-folder = Omdøb mappe ## Replace Dialog replace = Erstat replace-title = "{ $filename }" eksisterer allerede på denne placering. replace-warning = Vil du erstatte den med den du er ved at gemme? Hvis du erstatter den, overskriver du dens indhold. replace-warning-operation = Vil du erstatte den? Hvis du erstatter den, overskriver du dens indhold. original-file = Original fil replace-with = Erstat med apply-to-all = Anvend for alle keep-both = Behold begge skip = Spring over ## Set as Executable and Launch Dialog set-executable-and-launch = Indstil som eksekverbar fil og start set-executable-and-launch-description = Vil du indstille "{ $name }" som en eksekverbar fil og starte den? set-and-launch = Indstil og start ## Metadata Dialog owner = Ejer group = Gruppe other = Andet # Context Pages ## About ## Add Network Drive add-network-drive = Tilføj netværksdrev connect = Opret forbindelse connect-anonymously = Opret forbindelse anonymt connecting = Opretter forbindelse... domain = Domæne enter-server-address = Indtast serveradresse network-drive-description = Server-adresser inkluderer et protokolpræfiks og adresse. Eksempler: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Tilgængelige protokoller,Præfiks AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDav,dav:// or davs:// network-drive-error = Kan ikke tilgå netværksdrev password = Adgangskode remember-password = Husk adgangskode try-again = Forsøg igen username = Brugernavn ## Operations cancelled = Annulleret edit-history = Redigér historie history = Historie no-history = Ingen historik. pending = Afventer progress = { $percent }% progress-cancelled = { $percent }%, annulleret progress-paused = { $percent }%, sat på pause failed = Mislykkedes complete = Færdigt compressing = Komprimerer { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... compressed = Komprimeret { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" copy_noun = Kopi creating = Opretter "{ $name }" i "{ $parent }" created = Oprettet "{ $name }" i "{ $parent }" copying = Kopierer { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... copied = Kopieret { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" emptying-trash = Tømmer { trash } ({ $progress })... emptied-trash = Tømt { trash } extracting = Udpakker { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... extracted = Udpakket { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" setting-executable-and-launching = Indstiller "{ $name }" som eksekverbar fil og starter set-executable-and-launched = Indstillet "{ $name }" som eksekverbar fil og startet moving = Flytter { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... moved = Flyttet { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" renaming = Omdøber "{ $from }" til "{ $to }" renamed = Omdøbt "{ $from }" til "{ $to }" restoring = Genopretter { $items } { $items -> [one] objekt *[other] objekter } fra { trash } ({ $progress })... restored = Genoprettet { $items } { $items -> [one] objekt *[other] objekter } fra { trash } unknown-folder = ukendt mappe ## Open with menu-open-with = Åbn med... default-app = { $name } (standardindstilling) ## Show details show-details = Vis detaljer type = Type: { $mime } items = Objekter: { $items } item-size = Størrelse: { $size } item-created = Oprettet: { $created } item-modified = Ændret: { $modified } item-accessed = Tilgået: { $accessed } calculating = Beregner... ## Settings settings = Indstillinger ### Appearance appearance = Udseende theme = Tema match-desktop = Match skrivebord dark = Mørk light = Lys # Context menu add-to-sidebar = Tilføj til sidebjælke compress = Komprimér extract-here = Extract new-file = Ny fil... new-folder = Ny mappe... open-in-terminal = Åbn i terminal move-to-trash = Flyt til skraldespand restore-from-trash = Genopret fra skraldespand remove-from-sidebar = Fjern fra sidebjælke sort-by-name = Sortér efter navn sort-by-modified = Sortér efter ændret sort-by-size = Sortér efter størrelse sort-by-trashed = Sortér efter sletningsdato ## Desktop change-wallpaper = Skift baggrundsbillede... desktop-appearance = Skrivebordsudseende... display-settings = Skærmindstillinger... # Menu ## File file = Fil new-tab = Ny fane new-window = Nyt vindue rename = Omdøb... close-tab = Luk fane quit = Afslut ## Edit edit = Redigér cut = Klip copy = Kopiér paste = Sæt ind select-all = Vælg alt ## View zoom-in = Zoom ind default-size = Standardstørrelse zoom-out = Zoom ud view = Visning grid-view = Gitter-visning list-view = Liste-visning show-hidden-files = Vis skjulte filer list-directories-first = List mapper først gallery-preview = Galleri-forhåndsvisning menu-settings = Indstillinger... menu-about = Om COSMIC Filer... ## Sort sort = Sortér sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Nyeste først sort-oldest-first = Ældste først sort-smallest-to-largest = Mindste til største sort-largest-to-smallest = Største til mindste ================================================ FILE: i18n/de/cosmic_files.ftl ================================================ cosmic-files = COSMIC Dateien empty-folder = Leerer Ordner empty-folder-hidden = Leerer Ordner (hat versteckte Elemente) no-results = Keine Ergebnisse gefunden filesystem = Dateisystem home = Benutzerordner networks = Netzwerke notification-in-progress = Dateivorgänge werden ausgeführt trash = Papierkorb recents = Zuletzt verwendet undo = Rückgängig today = Heute # Optionen für die Desktop-Ansicht desktop-view-options = Optionen für die Desktop-Ansicht… show-on-desktop = Auf Desktop anzeigen desktop-folder-content = Inhalt des Desktop-Ordners mounted-drives = Eingehängte Laufwerke trash-folder-icon = Ordnersymbol des Papierkorbs icon-size-and-spacing = Symbolgröße und -abstand icon-size = Symbolgröße grid-spacing = Rasterabstand # Listenansicht name = Name modified = Geändert trashed-on = In den Papierkorb verschoben size = Größe # Fortschrittsfußzeile details = Details dismiss = Meldung verwerfen operations-running = { $running } { $running -> [one] laufender Vorgang *[other] laufende Vorgänge } ({ $percent } %)... operations-running-finished = { $running } { $running -> [one] laufender Vorgang *[other] laufende Vorgänge } ({ $percent } %), { $finished } abgeschlossen... pause = Pause resume = Fortsetzen # Dialoge ## Komprimieren-Dialog create-archive = Archiv erstellen ## Entpacken-Dialog extract-password-required = Passwort erforderlich extract-to = Entpacken nach... extract-to-title = In Ordner entpacken ## Dialog zum Leeren des Papierkorbs empty-trash = Papierkorb leeren empty-trash-warning = Elemente im Papierkorb werden endgültig gelöscht ## Einhängefehler-Dialog mount-error = Zugriff auf Laufwerk nicht möglich # Neue(r) Datei/Ordner-Dialog create-new-file = Neue Datei erstellen create-new-folder = Neuen Ordner erstellen file-name = Dateiname folder-name = Ordnername file-already-exists = Eine Datei mit diesem Namen existiert bereits folder-already-exists = Ein Ordner mit diesem Namen existiert bereits name-hidden = Mit „.“ beginnende Namen werden versteckt name-invalid = Name darf nicht „{ $filename }“ sein name-no-slashes = Name darf keine Schrägstriche enthalten # Öffnen/Speichern-Dialog cancel = Abbrechen create = Erstellen open = Öffnen open-file = Datei öffnen open-folder = Ordner öffnen open-in-new-tab = In neuem Tab öffnen open-in-new-window = In neuem Fenster öffnen open-item-location = Speicherort des Elements öffnen open-multiple-files = Mehrere Dateien öffnen open-multiple-folders = Mehrere Ordner öffnen save = Speichern save-file = Datei speichern ## Öffnen-mit-Dialog open-with-title = Wie möchtest du „{ $name }“ öffnen? browse-store = { $store } durchsuchen other-apps = Andere Anwendungen related-apps = Ähnliche Anwendungen ## Endgültig-löschen-Dialog selected-items = Die { $items } ausgewählten Elemente permanently-delete-question = Endgültig löschen? delete = Löschen permanently-delete-warning = { $target } wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. # Umbenennen-Dialog rename-file = Datei umbenennen rename-folder = Ordner umbenennen # Ersetzen-Dialog replace = Ersetzen replace-title = „{ $filename }“ existiert bereits an diesem Ort replace-warning = Möchtest du sie durch diejenige ersetzen, die du gerade speicherst? Beim Ersetzen wird ihr Inhalt überschrieben. replace-warning-operation = Möchtest du sie ersetzen? Beim Ersetzen wird ihr Inhalt überschrieben. original-file = Originaldatei replace-with = Ersetzen mit apply-to-all = Auf alle anwenden keep-both = Beide behalten skip = Überspringen ## Dialog zum Festlegen als ausführbar und starten set-executable-and-launch = Als ausführbar festlegen und starten set-executable-and-launch-description = Möchtest du „{ $name }“ als ausführbar festlegen und starten? set-and-launch = Festlegen und starten ## Metadaten-Dialog open-with = Öffnen mit owner = Eigentümer group = Gruppe other = Andere ### Modus 0 none = Keine ### Modus 1 (ungewöhnlich) execute-only = Nur ausführen ### Modus 2 (ungewöhnlich) write-only = Nur schreiben ### Modus 3 (ungewöhnlich) write-execute = Schreiben und ausführen ### Modus 4 read-only = Nur lesen ### Modus 5 read-execute = Lesen und ausführen ### Modus 6 read-write = Lesen und schreiben ### Modus 7 read-write-execute = Lesen, schreiben und ausführen ## Fehlerdialog zum gewünschten Pfad favorite-path-error = Fehler beim Öffnen des Verzeichnisses favorite-path-error-description = „{ $path }“ kann nicht geöffnet werden „{ $path }“ existiert möglicherweise nicht oder du hast keine Berechtigung zum Öffnen Möchtest du es aus der Seitenleiste entfernen? remove = Entfernen keep = Behalten # Kontextseiten ## Über ## Netzlaufwerk hinzufügen add-network-drive = Netzlaufwerk hinzufügen connect = Verbinden connect-anonymously = Anonym verbinden connecting = Wird verbunden... domain = Domain enter-server-address = Serveradresse eingeben network-drive-description = Serveradressen enthalten ein Protokollpräfix und eine Adresse. Beispiele: ssh://192.168.0.1, ftp://[2001:db8::1] ### Achte darauf, dass das Komma, das die Spalten trennt, erhalten bleibt network-drive-schemes = Verfügbare Protokolle,Präfix AppleTalk,afp:// File Transfer Protocol,ftp:// oder ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// oder ssh:// WebDav,dav:// oder davs:// network-drive-error = Zugriff auf Netzlaufwerk nicht möglich password = Passwort remember-password = Passwort merken try-again = Erneut versuchen username = Benutzername ## Vorgänge cancelled = Abgebrochen edit-history = Verlauf bearbeiten history = Verlauf no-history = Keine Einträge im Verlauf. pending = Ausstehend progress = { $percent } % progress-cancelled = { $percent } %, abgeschlossen progress-paused = { $percent } %, pausiert failed = Fehlgeschlagen complete = Abgeschlossen compressing = { $items } { $items -> [one] Element wird *[other] Elemente werden } von „{ $from }“ nach „{ $to }“ komprimiert ({ $progress })... compressed = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } von „{ $from }“ nach „{ $to }“ komprimiert copy_noun = Kopie creating = „{ $name }“ in „{ $parent }“ wird erstellt created = „{ $name }“ in „{ $parent }“ wurde erstellt copying = { $items } { $items -> [one] Element wird *[other] Elemente werden } von „{ $from }“ nach „{ $to }“ kopiert ({ $progress })... copied = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } „{ $from }“ nach „{ $to }“ kopiert deleting = { $items } { $items -> [one] Element wird *[other] Elemente werden } aus dem { trash } gelöscht ({ $progress })... deleted = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } aus dem { trash } gelöscht emptying-trash = { trash } wird geleert ({ $progress })... emptied-trash = { trash } geleert extracting = { $items } { $items -> [one] Element wird *[other] Elemente werden } von „{ $from }“ nach „{ $to }“ entpackt ({ $progress })... extracted = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } von „{ $from }“ nach „{ $to }“ entpackt setting-executable-and-launching = „{ $name }“ wird als ausführbar festgelegt und gestartet set-executable-and-launched = „{ $name }“ als ausführbar festgelegt und gestartet setting-permissions = Berechtigungen für „{ $name }“ werden auf { $mode } festgelegt set-permissions = Berechtigungen für „{ $name }“ auf { $mode } festlegen moving = { $items } { $items -> [one] Element wird *[other] Elemente werden } von „{ $from }“ nach „{ $to }“ verschoben ({ $progress })... moved = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } von „{ $from }“ nach „{ $to }“ verschoben permanently-deleting = { $items } { $items -> [one] Element wird *[other] Elemente werden } endgültig gelöscht permanently-deleted = { $items } { $items -> [one] Element wurde *[other] Element wurden } endgültig gelöscht renaming = „{ $from }“ wird in „{ $to }“ umbenannt renamed = „{ $from }“ wurde in „{ $to }“ umbenannt restoring = { $items } { $items -> [one] Element wird *[other] Elemente werden } aus dem { trash } wiederhergestellt ({ $progress })... restored = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } aus dem { trash } wiederhergestellt unknown-folder = unbekannter Ordner ## Öffnen mit menu-open-with = Öffnen mit... default-app = { $name } (Standard) ## Details anzeigen show-details = Details anzeigen type = Typ: { $mime } items = Elemente: { $items } item-size = Größe: { $size } item-created = Erstellt: { $created } item-modified = Geändert: { $modified } item-accessed = Zugegriffen: { $accessed } calculating = Wird berechnet... ## Einstellungen settings = Einstellungen single-click = Mit einem Klick öffnen ### Aussehen appearance = Aussehen theme = Thema match-desktop = An Desktop anpassen dark = Dunkel light = Hell ### Zum Suchen tippen type-to-search = Zum Suchen tippen type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein # Kontextmenü add-to-sidebar = Zur Seitenleiste hinzufügen compress = Komprimieren... delete-permanently = Endgültig löschen extract-here = Entpacken new-file = Neue Datei... new-folder = Neuer Ordner... open-in-terminal = Im Terminal öffnen move-to-trash = In den Papierkorb verschieben restore-from-trash = Aus dem Papierkorb wiederherstellen remove-from-sidebar = Von der Seitenleiste entfernen sort-by-name = Nach Name sortieren sort-by-modified = Nach Änderung sortieren sort-by-size = Nach Größe sortieren sort-by-trashed = Nach Löschzeitpunkt sortieren ## Desktop change-wallpaper = Hintergrundbild ändern... desktop-appearance = Desktop-Aussehen... display-settings = Anzeigeeinstellungen... # Menü ## Datei file = Datei new-tab = Neuer Tab new-window = Neues Fenster reload-folder = Ordner neu laden rename = Umbenennen... close-tab = Tab schließen quit = Beenden ## Bearbeiten edit = Bearbeiten cut = Ausschneiden copy = Kopieren paste = Einfügen select-all = Alles auswählen ## Ansicht zoom-in = Hineinzoomen default-size = Standardgröße zoom-out = Herauszoomen view = Ansicht grid-view = Rasteransicht list-view = Listenansicht show-hidden-files = Versteckte Dateien anzeigen list-directories-first = Verzeichnisse zuerst auflisten gallery-preview = Galerie-Vorschau menu-settings = Einstellungen... menu-about = Über COSMIC Dateien... ## Sortieren sort = Sortierung sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Neueste zuerst sort-oldest-first = Älteste zuerst sort-smallest-to-largest = Von klein nach groß sort-largest-to-smallest = Von groß nach klein repository = Repository empty-trash-title = Papierkorb leeren? copy-to-title = Kopierziel auswählen copy-to-button-label = Kopieren support = Unterstützung progress-failed = { $percent } %, fehlgeschlagen removing-from-recents = { $items } { $items -> [one] Element wird *[other] Elemente werden } aus { recents } entfernt removed-from-recents = { $items } { $items -> [one] Element wurde *[other] Elemente wurden } aus { recents } entfernt eject = Auswerfen type-to-search-select = Wählt die erste übereinstimmende Datei oder den ersten übereinstimmenden Ordner aus pasted-image = Eingefügtes Bild pasted-text = Eingefügter Text pasted-video = Eingefügtes Video copy-to = Kopieren nach... show-recents = Ordner zuletzt verwendeter Elemente in der Seitenleiste 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 ================================================ FILE: i18n/el/cosmic_files.ftl ================================================ empty-folder = Άδειος φάκελος no-results = Δεν βρέθηκαν αποτελέσματα trash = Κάδος Ανακύκλωσης recents = Πρόσφατα cosmic-files = COSMIC Αρχεία empty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά αντικείμενα) filesystem = Σύστημα αρχείων home = Προσωπικός φάκελος networks = Δίκτυο comment = Αρχεία για το COSMIC περιβάλλον keywords = Φάκελος;Διαχειριστής; rename = Μετονομασία... close-tab = Κλείσιμο καρτέλας light = Φωτεινό dark = Σκοτεινό ================================================ FILE: i18n/en/cosmic_files.ftl ================================================ cosmic-files = COSMIC Files comment = File manager for the COSMIC desktop keywords = Folder;Manager; empty-folder = Empty folder empty-folder-hidden = Empty folder (has hidden items) no-results = No results found filesystem = Filesystem home = Home networks = Networks notification-in-progress = File operations are in progress trash = Trash recents = Recents undo = Undo today = Today # Desktop view options desktop-view-options = Desktop view options... show-on-desktop = Show on Desktop desktop-folder-content = Desktop folder content mounted-drives = Mounted drives trash-folder-icon = Trash folder icon icon-size-and-spacing = Icon size and spacing icon-size = Icon size grid-spacing = Grid spacing # List view name = Name modified = Modified trashed-on = Trashed size = Size # Progress footer details = Details dismiss = Dismiss message operations-running = {$running} {$running -> [one] operation *[other] operations } running ({$percent}%)... operations-running-finished = {$running} {$running -> [one] operation *[other] operations } running ({$percent}%), {$finished} finished... pause = Pause resume = Resume # Dialogs ## Compress Dialog create-archive = Create archive ## Copy To Dialog copy-to-title = Select copy destination copy-to-button-label = Copy ## Extract Dialog extract-password-required = Password required extract-to = Extract To... extract-to-title = Extract to folder ## Empty Trash Dialog empty-trash = Empty trash empty-trash-title = Empty trash? empty-trash-warning = Items in the Trash folder will be permanently deleted ## Mount Error Dialog mount-error = Unable to access drive ## Move To Dialog move-to-title = Select move destination move-to-button-label = Move ## New File/Folder Dialog create-new-file = Create new file create-new-folder = Create new folder file-name = File name folder-name = Folder name file-already-exists = A file with that name already exists folder-already-exists = A folder with that name already exists name-hidden = Names starting with "." will be hidden name-invalid = Name cannot be "{$filename}" name-no-slashes = Name cannot contain slashes ## Open/Save Dialog cancel = Cancel create = Create open = Open open-file = Open file open-folder = Open folder open-in-new-tab = Open in new tab open-in-new-window = Open in new window open-item-location = Open item location open-multiple-files = Open multiple files open-multiple-folders = Open multiple folders save = Save save-file = Save file ## Open With Dialog open-with-title = How do you want to open "{$name}"? 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 permanently-delete-question = Permanently delete? delete = Delete permanently-delete-warning = {$target} will be permanently deleted. This action can't be undone. ## Rename Dialog rename-file = Rename file rename-folder = Rename folder ## Replace Dialog replace = Replace replace-title = "{$filename}" already exists in this location replace-warning = Do you want to replace it with the one you are saving? Replacing it will overwrite its content. replace-warning-operation = Do you want to replace it? Replacing it will overwrite its content. original-file = Original file replace-with = Replace with apply-to-all = Apply to all keep-both = Keep both skip = Skip ## Set as Executable and Launch Dialog set-executable-and-launch = Set as executable and launch set-executable-and-launch-description = Do you want to set "{$name}" as executable and launch it? set-and-launch = Set and launch ## Metadata Dialog open-with = Open with owner = Owner group = Group other = Other mixed = Mixed ### Mode 0 none = None ### Mode 1 (unusual) execute-only = Execute-only ### Mode 2 (unusual) write-only = Write-only ### Mode 3 (unusual) write-execute = Write and execute ### Mode 4 read-only = Read-only ### Mode 5 read-execute = Read and execute ### Mode 6 read-write = Read and write ### Mode 7 read-write-execute = Read, write, and execute ## Favorite Path Error Dialog favorite-path-error = Error opening directory favorite-path-error-description = Unable to open "{$path}" "{$path}" might not exist or you might not have permission to open it Would you like to remove it from the sidebar? remove = Remove keep = Keep # Context Pages ## About repository = Repository support = Support ## Add Network Drive add-network-drive = Add network drive connect = Connect connect-anonymously = Connect anonymously connecting = Connecting... domain = Domain enter-server-address = Enter server address network-drive-description = Server addresses include a protocol prefix and address. Examples: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Available protocols,Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDAV,dav:// or davs:// network-drive-error = Unable to access network drive password = Password remember-password = Remember password try-again = Try again username = Username ## Operations cancelled = Cancelled edit-history = Edit history history = History no-history = No items in history. pending = Pending progress = {$percent}% progress-cancelled = {$percent}%, cancelled progress-failed = {$percent}%, failed progress-paused = {$percent}%, paused failed = Failed complete = Complete compressing = Compressing {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" ({$progress})... compressed = Compressed {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" copy_noun = Copy pasted-image = Pasted Image pasted-text = Pasted Text pasted-video = Pasted Video creating = Creating "{$name}" in "{$parent}" created = Created "{$name}" in "{$parent}" copying = Copying {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" ({$progress})... copied = Copied {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" deleting = Deleting {$items} {$items -> [one] item *[other] items } from {trash} ({$progress})... deleted = Deleted {$items} {$items -> [one] item *[other] items } from {trash} emptying-trash = Emptying {trash} ({$progress})... emptied-trash = Emptied {trash} extracting = Extracting {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" ({$progress})... extracted = Extracted {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" setting-executable-and-launching = Setting "{$name}" as executable and launching set-executable-and-launched = Set "{$name}" as executable and launched setting-permissions = Setting permissions for "{$name}" to {$mode} set-permissions = Set permissions for "{$name}" to {$mode} moving = Moving {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" ({$progress})... moved = Moved {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" permanently-deleting = Permanently deleting {$items} {$items -> [one] item *[other] items } permanently-deleted = Permanently deleted {$items} {$items -> [one] item *[other] items } removing-from-recents = Removing {$items} {$items -> [one] item *[other] items } from {recents} removed-from-recents = Removed {$items} {$items -> [one] item *[other] items } from {recents} renaming = Renaming "{$from}" to "{$to}" renamed = Renamed "{$from}" to "{$to}" restoring = Restoring {$items} {$items -> [one] item *[other] items } from {trash} ({$progress})... restored = Restored {$items} {$items -> [one] item *[other] items } from {trash} unknown-folder = unknown folder ## Open with menu-open-with = Open with... default-app = {$name} (default) ## Show details show-details = Show details type = Type: {$mime} items = Items: {$items} item-size = Size: {$size} item-created = Created: {$created} item-modified = Modified: {$modified} item-accessed = Accessed: {$accessed} calculating = Calculating... ## Settings settings = Settings single-click = Single click to open show-recents = Recents folder in the sidebar ### Appearance appearance = Appearance theme = Theme match-desktop = Match desktop dark = Dark light = Light ### Type to search type-to-search = Type to search type-to-search-recursive = Searches the current folder and all subfolders type-to-search-enter-path = Enters the path to the directory or file type-to-search-select = Selects the first matching file or folder # Context menu add-to-sidebar = Add to sidebar clear-recents-history = Clear Recents history compress = Compress... copy-to = Copy to... delete-permanently = Delete permanently eject = Eject extract-here = Extract new-file = New file... new-folder = New folder... open-in-terminal = Open in terminal move-to = Move to... move-to-trash = Move to trash restore-from-trash = Restore from trash remove-from-sidebar = Remove from sidebar sort-by-name = Sort by name sort-by-modified = Sort by modified sort-by-size = Sort by size sort-by-trashed = Sort by delete time remove-from-recents = Remove from recents ## Desktop change-wallpaper = Change wallpaper... desktop-appearance = Desktop appearance... display-settings = Display settings... # Menu ## File file = File new-tab = New tab new-window = New window reload-folder = Reload folder rename = Rename... close-tab = Close tab quit = Quit ## Edit edit = Edit cut = Cut copy = Copy copy-path = Copy path paste = Paste select-all = Select all ## View zoom-in = Zoom in default-size = Default size zoom-out = Zoom out view = View grid-view = Grid view list-view = List view show-hidden-files = Show hidden files list-directories-first = List directories first gallery-preview = Gallery preview menu-settings = Settings... menu-about = About COSMIC Files... ## Sort sort = Sort sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Newest first sort-oldest-first = Oldest first sort-smallest-to-largest = Smallest to largest sort-largest-to-smallest = Largest to smallest ================================================ FILE: i18n/en-GB/cosmic_files.ftl ================================================ ================================================ FILE: i18n/eo/cosmic_files.ftl ================================================ ================================================ FILE: i18n/es/cosmic_files.ftl ================================================ cosmic-files = Archivos COSMIC comment = Gestor de archivos de COSMIC keywords = Archivos;Ficheros;Gestor;Explorador; empty-folder = Carpeta vacía empty-folder-hidden = Carpeta vacía (Contiene archivos ocultos) 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. trash = Papelera recents = Reciente undo = Deshacer today = Hoy # Desktop view options desktop-view-options = Opciones de vista del escritorio... show-on-desktop = Mostrar en el escritorio desktop-folder-content = Contenido de la carpeta del escritorio mounted-drives = Unidades montadas trash-folder-icon = Icono de la papelera icon-size-and-spacing = Tamaño y espaciado de los iconos icon-size = Tamaño del icono grid-spacing = Espaciado de la cuadrícula # List view name = Nombre modified = Modificado trashed-on = Enviado a la papelera size = Tamaño # Progress footer details = Detalles dismiss = Descartar mensaje operations-running = { $running } { $running -> [one] operación *[other] operaciones } ejecutándose ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operación *[other] operaciones } ejecutándose ({ $percent }%), { $finished } finished... pause = Pausar resume = Reanudar # Dialogs ## Compress Dialog create-archive = Crear archivo ## Extract Dialog extract-password-required = Contraseña requerida extract-to = Extraer en... extract-to-title = Extraer a una carpeta ## Empty Trash Dialog empty-trash = Vaciar la papelera empty-trash-warning = ¿Está seguro de que quiere eliminar permanentemente todos los archivos de la papelera? ## Mount Error Dialog mount-error = No se puede acceder a la unidad ## New File/Folder Dialog create-new-file = Crear nuevo archivo create-new-folder = Crear nueva carpeta file-name = Nombre del archivo folder-name = Nombre de la carpeta file-already-exists = Ya existe un archivo con ese nombre. folder-already-exists = Ya existe una carpeta con ese nombre. name-hidden = Nombres comenzando con "." serán ocultados. name-invalid = El nombre no puede ser: "{ $filename }". name-no-slashes = El nombre no puede contener slashes (barras). ## Open/Save Dialog cancel = Cancelar create = Crear open = Abrir open-file = Abrir archivo open-folder = Abrir carpeta open-in-new-tab = Abrir en nueva pestaña open-in-new-window = Abrir en nueva ventana open-item-location = Abrir ubicación del archivo open-multiple-files = Abrir multiples archivos open-multiple-folders = Abrir multiples carpetas save = Guardar save-file = Guardar archivo ## Open With Dialog open-with-title = ¿Cómo quiere abrir "{ $name }"? browse-store = Explorar { $store } other-apps = Otras aplicaciones related-apps = Aplicaciones relacionadas ## Permanently delete Dialog selected-items = los { $items } archivos seleccionados permanently-delete-question = Eliminar permanentemente delete = Eliminar permanently-delete-warning = ¿Quiere eliminar permanentemente { $target }? Esta acción no se puede deshacer. ## Rename Dialog rename-file = Renombrar archivo rename-folder = Renombrar carpeta ## Replace Dialog replace = Reemplazar replace-title = { $filename } ya existe en esta ruta. replace-warning = ¿Quiere remplazarlo con el que está guardando? Reemplazarlo sobrescribirá su contenido. replace-warning-operation = ¿Quieres reemplazarlo? Reemplazarlo sobrescribirá su contenido. original-file = Archivo original replace-with = Reemplazar con apply-to-all = Aplicar a todos keep-both = Conservar ambos skip = Saltar ## Set as Executable and Launch Dialog set-executable-and-launch = Establecer como ejecutable y ejecutar set-executable-and-launch-description = ¿Quieres establecer "{ $name }" como ejecutable y ejecutar? set-and-launch = Establecer y ejecutar ## Metadata Dialog open-with = Abrir con owner = Propietario group = Grupo other = Otro ### Mode 0 none = Ninguno ### Mode 1 (unusual) execute-only = Únicamente ejecución ### Mode 2 (unusual) write-only = Únicamente escritura ### Mode 3 (unusual) write-execute = Escritura y ejecución ### Mode 4 read-only = Únicamente lectura ### Mode 5 read-execute = Lectura y ejecución ### Mode 6 read-write = Lectura y escritura ### Mode 7 read-write-execute = Lectura, escritura y ejecución ## Favorite Path Error Dialog favorite-path-error = Error al abrir el directorio favorite-path-error-description = No se puede abrir "{ $path }". Puede que no exista o que no tenga permisos para abrirlo. ¿Quiere eliminarlo de la barra lateral? remove = Eliminar keep = Mantener # Context Pages ## About repository = Repositorio support = Apoyo ## Add Network Drive add-network-drive = Agregar una unidad de red connect = Conectar connect-anonymously = Conectar de forma anónima connecting = Conectando... domain = Dominio enter-server-address = Ingresa la dirección del servidor network-drive-description = Las direcciones de los servidores incluyen un prefijo de protocolo y una dirección. Ejemplos: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocolos disponibles,Prefijo AppleTalk,afp:// Protocolo de transferencia de archivos,ftp:// o ftps:// Sistema de archivos de red,nfs:// Bloque de mensajes del servidor,smb:// Protocolo de transferencia de archivos SSH,sftp:// o ssh:// WebDav,dav:// o davs:// network-drive-error = No se puede acceder a la unidad de red password = Contraseña remember-password = Recordar contraseña try-again = Intentar de nuevo username = Nombre de usuario ## Operations cancelled = Cancelada edit-history = Historial de ediciones history = Historial no-history = No hay archivos en el historial. pending = Pendiente progress = { $percent } % progress-cancelled = { $percent }%, cancelado progress-paused = { $percent }%, pausado failed = Fallidas complete = Completadas compressing = Comprimiendo { $items -> [one] elemento *[other] { $items } elementos } de "{ $from }" a "{ $to }" ({ $progress })... compressed = { $items -> [one] Se ha comprimido un elemento *[other] Se han comprimidos { $items } elementos } de "{ $from }" a "{ $to }" copy_noun = Copia creating = Creando "{ $name }" en "{ $parent }" created = Se ha creado "{ $name }" en "{ $parent }" copying = Copiando { $items -> [one] elemento *[other] { $items } elementos } de "{ $from }" a "{ $to }" ({ $progress })... copied = { $items -> [one] Se ha copiado un archivo *[other] Se han copiado { $items } archivos } de "{ $from }" a "{ $to }" deleting = { $items -> [one] Eliminando un archivo *[other] Eliminando { $items } archivos } de la { trash } ({ $progress })... deleted = { $items -> [one] Se ha eliminado un elemento *[other] Se han eliminado { $items } elementos } de la { trash } emptying-trash = Vaciando la { trash } ({ $progress })... emptied-trash = Se ha vaciado la { trash } extracting = Extrayendo { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... extracted = { $items -> [one] Se ha extraído un elemento *[other] Se han extraído { $items } elementos } de "{ $from }" a "{ $to }" setting-executable-and-launching = Estableciendo "{ $name }" como ejecutable y lanzando set-executable-and-launched = Se ha establecido "{ $name }" como ejecutable y se ha lanzado moving = Moviendo { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... moved = { $items -> [one] Se ha movido un elemento *[other] Se han movido { $items } elementos } de "{ $from }" a "{ $to }" permanently-deleting = Eliminando { $items } { $items -> [one] elemento *[other] archivos } permanentemente permanently-deleted = { $items -> [one] Se ha eliminado un archivo permanentemente *[other] Se han eliminado { $items } archivos permanentemente } renaming = Cambiando el nombre de "{ $from }" a "{ $to }" renamed = Se ha cambiado el nombre de "{ $from }" a "{ $to }" restoring = Restaurando { $items } { $items -> [one] elemento *[other] elementos } de la { trash } ({ $progress })... restored = { $items -> [one] Se ha restaurado un archivo *[other] Se han restaurado { $items } archivos } de la { trash } unknown-folder = carpeta desconocida ## Open with menu-open-with = Abrir con... default-app = { $name } (predeterminado) ## Show details show-details = Mostrar detalles type = Tipo: { $mime } items = Archivos: { $items } item-size = Tamaño: { $size } item-created = Fecha de creación: { $created } item-modified = Última modificación: { $modified } item-accessed = Último acceso: { $accessed } calculating = Calculando... ## Settings settings = Configuración single-click = Abrir con solo un clic ### Appearance appearance = Apariencia theme = Tema match-desktop = Seguir el estilo del escritorio dark = Oscuro light = Claro ### Type to Search type-to-search = Escriba para buscar type-to-search-recursive = Buscar en la carpeta actual y todas sus subcarpetas type-to-search-enter-path = Escriba la ruta del directorio o archivo # Context menu add-to-sidebar = Añadir a la barra lateral compress = Comprimir delete-permanently = Eliminar permanentemente eject = Expulsar extract-here = Extraer aquí new-file = Nuevo archivo... new-folder = Nueva carpeta... open-in-terminal = Abrir en la consola move-to-trash = Mover a la papelera restore-from-trash = Restaurar de la papelera remove-from-sidebar = Quitar de la barra lateral sort-by-name = Ordenar por nombre sort-by-modified = Ordenar por fecha de modificación sort-by-size = Ordenar por tamaño sort-by-trashed = Ordenar por fecha de eliminación remove-from-recents = Quitar de recientes ## Desktop change-wallpaper = Cambiar fondo de pantalla... desktop-appearance = Apariencia del escritorio... display-settings = Configuración de pantalla... # Menu ## File file = Archivo new-tab = Nueva pestaña new-window = Nueva ventana reload-folder = Refrescar carpeta rename = Renombrar... close-tab = Cerrar pestaña quit = Cerrar ## Edit edit = Editar cut = Cortar copy = Copiar paste = Pegar select-all = Seleccionar todo ## View zoom-in = Ampliar default-size = Tamaño predeterminado zoom-out = Disminuir view = Vista grid-view = Vista de cuadrícula list-view = Vista de lista show-hidden-files = Mostrar archivos ocultos list-directories-first = Enumerar los directorios primero gallery-preview = Vista previa de la galería menu-settings = Configuración... menu-about = Acerca de COSMIC Files... ## Sort sort = Ordenar sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Más reciente primero sort-oldest-first = Más antiguo primero sort-smallest-to-largest = De menor a mayor sort-largest-to-smallest = De mayor a menor removing-from-recents = Quitando { $items } { $items -> [one] elemento *[other] elementos } de { recents } removed-from-recents = { $items -> [one] Se ha quitado elemento *[other] Se han quitado { $items } elementos } de { recents } setting-permissions = Estableciendo permisos para "{ $name } " a { $mode } progress-failed = { $percent } %, con errores set-permissions = Establecer permisos de "{ $name }" como { $mode } ================================================ FILE: i18n/es-419/cosmic_files.ftl ================================================ cosmic-files = Archivos de COSMIC empty-folder = Carpeta vacía empty-folder-hidden = Carpeta vacía (tiene elementos ocultos) 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 trash = Papelera recents = Recientes undo = Deshacer today = Hoy # Desktop view options desktop-view-options = Opciones de vista del escritorio... show-on-desktop = Mostrar en el escritorio desktop-folder-content = Contenido de la carpeta del escritorio mounted-drives = Unidades montadas trash-folder-icon = Icono de la papelera icon-size-and-spacing = Tamaño y espaciado de los iconos icon-size = Tamaño del icono # List view name = Nombre modified = Modificado trashed-on = Enviado a la papelera size = Tamaño # Dialogs ## Compress Dialog create-archive = Crear archivo comprimido ## Empty Trash Dialog empty-trash = Vaciar la papelera empty-trash-warning = ¿Estás seguro de que deseas eliminar permanentemente todos los elementos de la papelera? ## New File/Folder Dialog create-new-file = Crear nuevo archivo create-new-folder = Crear nueva carpeta file-name = Nombre de archivo folder-name = Nombre de carpeta file-already-exists = Ya existe un archivo con ese nombre. folder-already-exists = Ya existe una carpeta con ese nombre. name-hidden = Los nombres que comiencen con "." se ocultarán. name-invalid = El nombre no puede ser "{ $filename }". name-no-slashes = El nombre no puede contener barras. ## Open/Save Dialog cancel = Cancelar create = Crear open = Abrir open-file = Abrir archivo open-folder = Abrir carpeta open-in-new-tab = Abrir en una nueva pestaña open-in-new-window = Abrir en una nueva ventana open-item-location = Abrir ubicación del elemento open-multiple-files = Abrir múltiples archivos open-multiple-folders = Abrir múltiples carpetas save = Guardar save-file = Guardar archivo ## Open With Dialog open-with-title = ¿Cómo deseas abrir "{ $name }"? browse-store = Explorar { $store } ## Rename Dialog rename-file = Cambiar el nombre del archivo rename-folder = Cambiar el nombre de la carpeta ## Replace Dialog replace = Reemplazar replace-title = "{ $filename }" ya existe en esta ubicación. replace-warning = ¿Quieres reemplazarlo con el que estás guardando? Reemplazarlo sobrescribirá su contenido. replace-warning-operation = ¿Quieres reemplazarlo? Reemplazarlo sobrescribirá su contenido. original-file = Archivo original replace-with = Reemplazar con apply-to-all = Aplicar a todos keep-both = Conservar ambos skip = Saltar ## Set as Executable and Launch Dialog set-executable-and-launch = Establecer como ejecutable y ejecutar set-executable-and-launch-description = ¿Deseas establecer "{ $name }" como ejecutable y abrirlo? set-and-launch = Establecer y ejecutar ## Metadata Dialog owner = Propietario group = Grupo other = Otro # Context Pages ## About ## Add Network Drive add-network-drive = Agregar unidad de red connect = Conectar connect-anonymously = Conectar de forma anónima connecting = Conectando... domain = Dominio enter-server-address = Ingresa la dirección del servidor network-drive-description = Las direcciones de los servidores incluyen un prefijo de protocolo y una dirección. Ejemplos: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocolos disponibles,Prefijo AppleTalk,afp:// Protocolo de transferencia de archivos,ftp:// o ftps:// Sistema de archivos de red,nfs:// Bloque de mensajes del servidor,smb:// Protocolo de transferencia de archivos SSH,sftp:// o ssh:// WebDav,dav:// o davs:// network-drive-error = No se puede acceder a la unidad de red password = Contraseña remember-password = Recordar contraseña try-again = Intentar de nuevo username = Nombre de usuario ## Operations edit-history = Historial de ediciones history = Historial no-history = No hay elementos en el historial. pending = Pendientes failed = Con error complete = Completadas compressing = Comprimiendo { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... compressed = { $items -> [one] Se ha comprimido un elemento *[other] Se han comprimidos { $items } elementos } de "{ $from }" a "{ $to }" copy_noun = Copiar creating = Creando "{ $name }" en "{ $parent }" created = Se ha creado "{ $name }" en "{ $parent }" copying = Copiando { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... copied = { $items -> [one] Se ha copiado un elemento *[other] Se han copiado { $items } elementos } de "{ $from }" a "{ $to }" emptying-trash = Vaciando la { trash } ({ $progress })... emptied-trash = Se ha vaciado la { trash } extracting = Extrayendo { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... extracted = { $items -> [one] Se ha extraído un elemento *[other] Se han extraído { $items } elementos } de "{ $from }" a "{ $to }" setting-executable-and-launching = Estableciendo "{ $name }" como ejecutable y abriendo set-executable-and-launched = Se ha establecido "{ $name }" como ejecutable y se ha abierto moving = Moviendo { $items } { $items -> [one] elemento *[other] elementos } de "{ $from }" a "{ $to }" ({ $progress })... moved = { $items -> [one] Se ha movido un elemento *[other] Se han movido { $items } elementos } de "{ $from }" a "{ $to }" renaming = Cambiando el nombre de "{ $from }" a "{ $to }" renamed = Se ha cambiado el nombre de "{ $from }" a "{ $to }" restoring = Restaurando { $items } { $items -> [one] elemento *[other] elementos } de la { trash } ({ $progress })... restored = { $items -> [one] Se ha restaurado un elemento *[other] Se han restaurado { $items } elementos } de la { trash } unknown-folder = carpeta desconocida ## Open with menu-open-with = Abrir con... default-app = { $name } (predeterminado) ## Show details show-details = Mostrar detalles ## Settings settings = Configuración ### Appearance appearance = Apariencia theme = Tema match-desktop = Igual que el escritorio dark = Oscuro light = Claro # Context menu add-to-sidebar = Añadir a la barra lateral compress = Comprimir extract-here = Extraer aquí new-file = Archivo nuevo... new-folder = Carpeta nueva... open-in-terminal = Abrir en una terminal move-to-trash = Mover a la papelera restore-from-trash = Restaurar de la papelera remove-from-sidebar = Quitar de la barra lateral sort-by-name = Ordenar por nombre sort-by-modified = Ordenar por modificado sort-by-size = Ordenar por tamaño sort-by-trashed = Ordenar por fecha de eliminación ## Desktop change-wallpaper = Cambiar fondo de pantalla... desktop-appearance = Apariencia del escritorio... display-settings = Configuración de pantalla... # Menu ## File file = Archivo new-tab = Nueva pestaña new-window = Nueva ventana rename = Cambiar nombre... close-tab = Cerrar pestaña quit = Cerrar ## Edit edit = Editar cut = Cortar copy = Copiar paste = Pegar select-all = Seleccionar todo ## View zoom-in = Ampliar default-size = Tamaño predeterminado zoom-out = Disminuir view = Vistar grid-view = Vista de cuadrícula list-view = Vista de lista show-hidden-files = Mostrar archivos ocultos list-directories-first = Mostrar directorios primero gallery-preview = Vista previa de la galería menu-settings = Configuración... menu-about = Acerca de archivos COSMIC... ## Sort sort = Ordenar sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Más reciente primero sort-oldest-first = Más antiguo primero sort-smallest-to-largest = De menor a mayor sort-largest-to-smallest = De mayor a menor repository = Repositorio support = Apoyo details = Detalles dismiss = Descartar mensaje remove = Eliminar cancelled = Canceladas grid-spacing = Espaciado de cuadrícula operations-running = { $running -> [one] Operación de { $running } *[other] Operaciones de { $running } } en ejecución ({ $percent } %)... operations-running-finished = { $running -> [one] Operación de { $running } *[other] Operaciones de { $running } } en ejecución ({ $percent } %), { $finished } completada(s)... pause = Pausar resume = Reanudar extract-password-required = Contraseña requerida extract-to = Extraer en... extract-to-title = Extraer a una carpeta mount-error = No se puede acceder a la unidad other-apps = Otras aplicaciones related-apps = Aplicaciones relacionadas selected-items = los { $items } archivos seleccionados permanently-delete-question = Eliminar de forma permanente delete = Eliminar permanently-delete-warning = ¿Estás seguro de que quieres eliminar { $target } de forma permanente? Esta acción no se puede deshacer. open-with = Abrir con none = Ninguno execute-only = Solo ejecución write-only = Solo escritura write-execute = Escritura y ejecución read-only = Solo lectura read-execute = Lectura y ejecución read-write = Lectura y escritura read-write-execute = Lectura, escritura y ejecución favorite-path-error = Error al abrir el directorio favorite-path-error-description = No se puede abrir "{ $path }". Puede que no exista o que no tengas permiso a abrirlo. ¿Quieres eliminarlo de la barra lateral? keep = Reservar progress = { $percent } % progress-cancelled = { $percent } %, cancelado progress-failed = { $percent } %, con errores progress-paused = { $percent } %, pausado setting-permissions = Estableciendo permisos de "{ $name }" como { $mode } set-permissions = Establecer permisos de "{ $name }" como { $mode } permanently-deleting = Eliminando { $items } { $items -> [one] elemento *[other] elementos } de forma permanente deleting = Eliminando { $items } { $items -> [one] elemento *[other] elementos } de la { trash } ({ $progress })... deleted = { $items -> [one] Se ha eliminado un elemento *[other] Se han eliminado { $items } elementos } de la { trash } permanently-deleted = { $items -> [one] Se ha eliminado un elemento *[other] Se han eliminado { $items } elementos } de forma permanente removing-from-recents = Quitando { $items } { $items -> [one] elemento *[other] elementos } de { recents } removed-from-recents = { $items -> [one] Se ha quitado elemento *[other] Se han quitado { $items } elementos } de { recents } reload-folder = Recargar la carpeta remove-from-recents = Quitar de recientes calculating = Calculando... type = Tipo: { $mime } items = Elementos: { $items } item-size = Tamaños: { $size } item-created = Creado: { $created } item-modified = Modificado: { $modified } item-accessed = Accedido: { $accessed } single-click = Abrir con un solo clic type-to-search = Escribir para buscar type-to-search-recursive = Buscar en la carpeta actual y todas las subcarpetas type-to-search-enter-path = Introducir la ruta al directorio o archivo delete-permanently = Eliminar de forma permanente eject = Expulsar ================================================ FILE: i18n/es-MX/cosmic_files.ftl ================================================ ================================================ FILE: i18n/et/cosmic_files.ftl ================================================ empty-folder = Tühi kaust empty-folder-hidden = Tühi kaust (leidub peidetud kirjeid) no-results = Tulemusi ei leidu filesystem = Failisüsteem home = Avaleht networks = Võrguühendused open-file = Ava fail appearance = Välimus theme = Kujundus match-desktop = Sobita töölauaga dark = Tume kujundus light = Hele kujundus open-folder = Ava kaust settings = Seadistused file = Fail quit = Välju connect = Ühenda support = Kasutajatugi delete = Kustuta name = Nimi new-tab = Uus vahekaart new-window = Uus aken close-tab = Sulge vahekaart edit = Muuda copy = Kopeeri paste = Aseta select-all = Vali kõik cancel = Katkesta open = Ava remove = Eemalda create = Loo repository = Tarkvarahoidla replace = Asenda save = Salvesta undo = Võta tegevus tagasi password = Salasõna cut = Lõika username = Kasutajanimi view = Vaata trash = Prügikast recents = Viimati kasutatud today = Täna menu-settings = Seadistused... modified = Muudetud trashed-on = Visatud prügikasti size = Suurus details = Üksikasjad pause = Peata resume = Jätka skip = Jäta vahele zoom-in = Suumi sisse default-size = Tavasuurus zoom-out = Suumi välja save-file = Salvesta fail create-new-folder = Lisa uus kaust create-new-file = Lisa uus fail file-name = Failinimi folder-name = Kausta nimi file-already-exists = Sellise nimega fail on juba olemas. folder-already-exists = Sellise nimega kaust on juba olemas. name-no-slashes = Nimes ei tohi olla kaldkriipse. show-on-desktop = Näita töölaual desktop-folder-content = Töölauakausta sisu mounted-drives = Haagitud andmekandjad trash-folder-icon = Prügikastikausta ikoon icon-size-and-spacing = Ikooni suurus ja vahed icon-size = Ikooni suurus grid-spacing = Vahed ruudustikus operations-running = { $running } { $running -> [one] tegevus *[other] tegevust } on töös ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] tegevus *[other] tegevust } on töös ({ $percent }%), { $finished } lõppenud... name-invalid = Nimi ei saa olla „{ $filename }“. name-hidden = Kui nime alguses on punkt, siis fail või kaust muutub peidetuks. open-in-new-tab = Ava uuel vahekaardil open-in-new-window = Ava uues aknas open-item-location = Ava objekti asukoht open-multiple-files = Ava mitu faili open-multiple-folders = Ava mitu kausta cancelled = Katkestatud pending = Ootel failed = Ebaõnnestunud complete = Tehtud dismiss = Suulge sõnum notification-in-progress = Tegevused failidega on pooleli. desktop-view-options = Töölauavaate valikud... create-archive = Loo arhiivifail extract-password-required = Salasõna on vajalik extract-to = Paki lahti siia... extract-to-title = Paku lahti kausta empty-trash = Tühjenda prügikast empty-trash-warning = Kas oled kindel, et soovid jäädavalt kustutada prügikasti sisu? related-apps = Seotud rakendused rename-file = Muuda faili nime rename-folder = Muuda kausta nime replace-title = „{ $filename }“ fail juba on selles asukohas olemas. open-with-title = Kuidas sa sooviksid avada „{ $name }“ faili? browse-store = Vaata siia: { $store } other-apps = Muud rakendused selected-items = { $items } valitud objekt(i) permanently-delete-question = Kas kustutame jäädavalt? permanently-delete-warning = Kas sa oled kindel, et soovid jäädavalt kustutada: { $target }? Seda tegevust ei saa tagasi pöörata. favorite-path-error = Viga kausta avamisel add-network-drive = Lisa võrguketas connect-anonymously = Ühenda anonüümselt connecting = Ühendamisel... domain = Domeen enter-server-address = Sisesta serveri aadress remember-password = Jäta salasõna meelde try-again = Proovi uuesti edit-history = Muutmiste ajalugu history = Ajalugu no-history = Ajaloos pole ühtegi kirjet. progress = { $percent }% progress-cancelled = { $percent }%, katkestatud progress-failed = { $percent }%, ebaõnnestus progress-paused = { $percent }%, peatatud unknown-folder = tundmatu kaust menu-open-with = Ava rakendusega… default-app = { $name } (vaikimisi) show-details = Näita üksikasju type = Tüüp: { $mime } items = Objekte: { $items } item-size = Suurus: { $size } item-created = Loodud: { $created } item-modified = Muudetud: { $modified } item-accessed = Kasutatud: { $accessed } calculating = Arvutan... single-click = Ava ühe klõpsuga reload-folder = Laadi kaust uuesti rename = Muuda nime... grid-view = Ruudustikuvaade list-view = Loendivaade show-hidden-files = Näita peidetud faile list-directories-first = Lisa kaustad loendi algusesse gallery-preview = Galerii eelvaade cosmic-files = COSMICu failid mount-error = Ligipääs andmekandjale puudub replace-warning = Kas sa soovid ta asendada sellega, mida oled salvestamas? Samaga asendub ka kogu sisu. replace-warning-operation = Kas sa soovid ta asendada? Samaga asendub ka kogu sisu. original-file = Algfail replace-with = Asenda järgnevaga apply-to-all = Kohalda kõigile keep-both = Jäta mõlemad alles set-executable-and-launch = Märgi käivitatavaks ja käivita set-executable-and-launch-description = Kas sa soovid „{ $name }“ faili märkida käivitatavaks ja ta käivitada? set-and-launch = Märgi ja käivita open-with = Ava rakendusega owner = Omanik group = Grupp other = Teised none = Määramata execute-only = Ainult käivitatav write-only = Ainult kirjutatav write-execute = Kirjutatav ja käivitatav read-only = Ainult loetav read-execute = Loetav ja käivitatav read-write = Loetav ja kirjutatav read-write-execute = Loetav, kirjutatav ja käivitatav favorite-path-error-description = „{ $path }“ asukoha avamine ei õnnestu. Teda kas pole olemas või sul pole õigusi tema avamiseks. Kas sooviksid ta külgribalt eemaldada? keep = Säilita network-drive-description = Serveri aadressides peab olema protokolli eesliide ja aadress ise. Näited: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Kasutatavad protokollid,eesliide AppleTalk,afp:// File Transfer Protocol,ftp:// või ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// või ssh:// WebDAV,dav:// või davs:// network-drive-error = Puudub ligipääs võrgus asuvale andmekandjale type-to-search = Otsimiseks kirjuta type-to-search-recursive = Otsing sellest kaustast ja alamkaustadest type-to-search-enter-path = Sisestab kausta või faili asukoha add-to-sidebar = Lisa külgribale remove-from-sidebar = Eemalda külgribalt copy_noun = Kopeeri creating = Loon: „{ $name }“ asukohas „{ $parent }“ created = „{ $name }“ on loodud asukohta „{ $parent }“ compress = Paki kokku delete-permanently = Kustuta jäädavalt eject = Väljasta extract-here = Paki lahti new-file = Uus fail... new-folder = Uus kaust... open-in-terminal = Ava terminalis move-to-trash = Viska prügikasti restore-from-trash = Taasta prügikastist sort-by-name = Järjesta nime alusel sort-by-modified = Järjesta muutmise alusel sort-by-size = Järjesta suuruse alusel sort-by-trashed = Järjesta kustutamise aja alusel remove-from-recents = Eemalda hiljutiste failide loendist change-wallpaper = Muuda taustapilti... desktop-appearance = Töölaua välimus... display-settings = Ekraani seadistused... menu-about = Rakenduse teave: COSMICu failid... sort = Järjesta sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Esmalt uuemad sort-oldest-first = Esmalt vanemad sort-smallest-to-largest = Väiksemast suuremani sort-largest-to-smallest = Suuremast väiksemani ================================================ FILE: i18n/eu/cosmic_files.ftl ================================================ ================================================ FILE: i18n/fa/cosmic_files.ftl ================================================ cosmic-files = فایل‌های COSMIC empty-folder = پوشه خالی empty-folder-hidden = پوشه خالی (شامل موارد مخفی) no-results = نتیجه‌ای یافت نشد filesystem = فایل سیستم home = خانه networks = شبکه‌ها notification-in-progress = عملیات فایل در حال انجام است. trash = زباله‌دان recents = موارد اخیر undo = بازگردانی today = امروز # Desktop view options desktop-view-options = گزینه‌های نمایش دسکتاپ... show-on-desktop = نمایش در دسکتاپ desktop-folder-content = محتوای پوشه دسکتاپ mounted-drives = درایوهای متصل‌شده trash-folder-icon = آیکون پوشه زباله‌دان icon-size-and-spacing = اندازه و فاصله آیکون‌ها icon-size = اندازه آیکون grid-spacing = فاصله شبکه‌ای # List view name = نام modified = زمان تغییر trashed-on = زمان حذف size = حجم # Progress footer details = جزئیات dismiss = بستن پیام operations-running = { $running } عملیات در حال اجرا ({ $percent }%)... operations-running-finished = { $running } عملیات در حال اجرا ({ $percent }%)، { $finished } پایان یافته... pause = توقف resume = ادامه # Dialogs ## Compress Dialog create-archive = ایجاد بایگانی ## Extract Dialog extract-password-required = رمز عبور مورد نیاز است extract-to = استخراج در... extract-to-title = استخراج در پوشه ## Empty Trash Dialog empty-trash = خالی کردن زباله‌دان empty-trash-warning = آیا از حذف دائمی همه موارد در زباله‌دان مطمئن هستید؟ ## Mount Error Dialog mount-error = دسترسی به درایو ممکن نیست ## New File/Folder Dialog create-new-file = ایجاد فایل جدید create-new-folder = ایجاد پوشه جدید file-name = نام فایل folder-name = نام پوشه file-already-exists = فایلی با این نام وجود دارد. folder-already-exists = پوشه‌ای با این نام وجود دارد. name-hidden = نام‌هایی که با "." شروع شوند مخفی خواهند بود. name-invalid = نام نمی‌تواند "{ $filename }" باشد. name-no-slashes = نام نمی‌تواند حاوی اسلش باشد. ## Open/Save Dialog cancel = لغو create = ایجاد open = باز کردن open-file = باز کردن فایل open-folder = باز کردن پوشه open-in-new-tab = باز کردن در زبانه جدید open-in-new-window = باز کردن در پنجره جدید open-item-location = باز کردن محل مورد open-multiple-files = باز کردن چندین فایل open-multiple-folders = باز کردن چندین پوشه save = ذخیره save-file = ذخیره فایل ## Open With Dialog open-with-title = چگونه می‌خواهید "{ $name }" را باز کنید؟ browse-store = مرور { $store } other-apps = برنامه‌های دیگر related-apps = برنامه‌های مرتبط ## Permanently delete Dialog selected-items = { $items } مورد انتخاب شده permanently-delete-question = حذف دائمی delete = حذف permanently-delete-warning = آیا از حذف دائمی { $target } مطمئن هستید؟ این عمل قابل بازگشت نیست. ## Rename Dialog rename-file = تغییر نام فایل rename-folder = تغییر نام پوشه ## Replace Dialog replace = جایگزین کردن replace-title = "{ $filename }" از قبل در این مکان وجود دارد. replace-warning = آیا می‌خواهید آن را با موردی که در حال ذخیره هستید جایگزین کنید؟ این عمل محتوای آن را بازنویسی خواهد کرد. replace-warning-operation = آیا می‌خواهید آن را جایگزین کنید؟ این عمل محتوای آن را بازنویسی خواهد کرد. original-file = فایل اصلی replace-with = جایگزینی با apply-to-all = اعمال برای همه keep-both = نگه‌داشتن هر دو skip = رد کردن ## Set as Executable and Launch Dialog set-executable-and-launch = تنظیم به عنوان فایل اجرایی و اجرا set-executable-and-launch-description = آیا می‌خواهید "{ $name }" را به عنوان فایل اجرایی تنظیم کرده و اجرا کنید؟ set-and-launch = تنظیم و اجرا ## Metadata Dialog open-with = باز کردن با owner = مالک group = گروه other = سایر ### Mode 0 none = هیچ‌کدام ### Mode 1 (unusual) execute-only = فقط اجرا ### Mode 2 (unusual) write-only = فقط نوشتن ### Mode 3 (unusual) write-execute = نوشتن و اجرا ### Mode 4 read-only = فقط خواندن ### Mode 5 read-execute = خواندن و اجرا ### Mode 6 read-write = خواندن و نوشتن ### Mode 7 read-write-execute = خواندن، نوشتن و اجرا ## Favorite Path Error Dialog favorite-path-error = خطا در باز کردن مسیر favorite-path-error-description = باز کردن "{ $path }" امکان‌پذیر نیست. ممکن است وجود نداشته باشد یا شما اجازه باز کردن آن را نداشته باشید. آیا می‌خواهید آن را از نوار کناری حذف کنید؟ remove = حذف keep = نگه داشتن # Context Pages ## About ## Add Network Drive add-network-drive = افزودن درایو شبکه connect = اتصال connect-anonymously = اتصال ناشناس connecting = در حال اتصال... domain = دامنه enter-server-address = آدرس سرور را وارد کنید network-drive-description = آدرس‌های سرور شامل پیشوند پروتکل و آدرس هستند. نمونه: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns 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:// network-drive-error = دسترسی به درایو شبکه ممکن نیست password = رمز عبور remember-password = به خاطر سپردن رمز عبور try-again = تلاش دوباره username = نام کاربری ## Operations cancelled = لغو شد edit-history = ویرایش تاریخچه history = تاریخچه no-history = هیچ موردی در تاریخچه وجود ندارد. pending = در حال انتظار progress = { $percent }% progress-cancelled = { $percent }%، لغو شد progress-paused = { $percent }%، متوقف شد failed = ناموفق complete = کامل شد compressing = در حال فشرده‌سازی { $items } مورد از "{ $from }" به "{ $to }" ({ $progress })... compressed = { $items } مورد از "{ $from }" به "{ $to }" فشرده شد copy_noun = کپی creating = در حال ایجاد "{ $name }" در "{ $parent }" created = "{ $name }" در "{ $parent }" ایجاد شد copying = در حال کپی { $items } مورد از "{ $from }" به "{ $to }" ({ $progress })... copied = { $items } مورد از "{ $from }" به "{ $to }" کپی شد deleting = در حال حذف { $items } مورد از { trash } ({ $progress })... deleted = { $items } مورد از { trash } حذف شد emptying-trash = در حال خالی کردن { trash } ({ $progress })... emptied-trash = { trash } خالی شد extracting = در حال استخراج { $items } مورد از "{ $from }" به "{ $to }" ({ $progress })... extracted = { $items } مورد از "{ $from }" به "{ $to }" استخراج شد setting-executable-and-launching = در حال تنظیم "{ $name }" به عنوان فایل اجرایی و اجرا set-executable-and-launched = "{ $name }" به عنوان فایل اجرایی تنظیم و اجرا شد setting-permissions = در حال تنظیم مجوزهای "{ $name }" به { $mode } set-permissions = مجوزهای "{ $name }" به { $mode } تنظیم شد moving = در حال انتقال { $items } مورد از "{ $from }" به "{ $to }" ({ $progress })... moved = { $items } مورد از "{ $from }" به "{ $to }" منتقل شد permanently-deleting = در حال حذف دائمی { $items } مورد permanently-deleted = { $items } مورد به صورت دائمی حذف شد removing-from-recents = در حال حذف { $items } مورد از { recents } removed-from-recents = { $items } مورد از { recents } حذف شد renaming = تغییر نام "{ $from }" به "{ $to }" renamed = "{ $from }" به "{ $to }" تغییر نام یافت restoring = در حال بازیابی { $items } مورد از { trash } ({ $progress })... restored = { $items } مورد از { trash } بازیابی شد unknown-folder = پوشه ناشناس ## Open with menu-open-with = باز کردن با... default-app = { $name } (پیش‌فرض) ## Show details show-details = نمایش جزئیات type = نوع: { $mime } items = مورد: { $items } item-size = حجم: { $size } item-created = ایجاد شده: { $created } item-modified = تغییر یافته: { $modified } item-accessed = دسترسی یافته: { $accessed } calculating = در حال محاسبه... ## Settings settings = تنظیمات single-click = باز کردن با یک کلیک ### Appearance appearance = ظاهر theme = پوسته match-desktop = مطابق با دسکتاپ dark = تاریک light = روشن ### Type to Search type-to-search = جستجو با تایپ کردن type-to-search-recursive = جستجو در پوشه فعلی و تمام زیرپوشه‌ها type-to-search-enter-path = وارد کردن مسیر پوشه یا فایل # Context menu add-to-sidebar = افزودن به نوار کناری compress = فشرده‌سازی delete-permanently = حذف دائمی eject = خارج کردن extract-here = استخراج new-file = فایل جدید... new-folder = پوشه جدید... open-in-terminal = باز کردن در ترمینال move-to-trash = انتقال به زباله‌دان restore-from-trash = بازیابی از زباله‌دان remove-from-sidebar = حذف از نوار کناری sort-by-name = مرتب‌سازی بر اساس نام sort-by-modified = مرتب‌سازی بر اساس زمان تغییر sort-by-size = مرتب‌سازی بر اساس حجم sort-by-trashed = مرتب‌سازی بر اساس زمان حذف remove-from-recents = حذف از موارد اخیر ## Desktop change-wallpaper = تغییر تصویر پس‌زمینه... desktop-appearance = ظاهر دسکتاپ... display-settings = تنظیمات نمایش... # Menu ## File file = فایل new-tab = زبانه جدید new-window = پنجره جدید reload-folder = بازخوانی پوشه rename = تغییر نام... close-tab = بستن زبانه quit = خروج ## Edit edit = ویرایش cut = برش copy = کپی paste = جایگذاری select-all = انتخاب همه ## View zoom-in = بزرگ‌نمایی default-size = اندازه پیش‌فرض zoom-out = کوچک‌نمایی view = نمایش grid-view = نمایش شبکه‌ای list-view = نمایش فهرستی show-hidden-files = نمایش فایل‌های مخفی list-directories-first = نمایش پوشه‌ها در ابتدا gallery-preview = پیش‌نمایش گالری menu-settings = تنظیمات... 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 = بزرگ‌ترین به کوچک‌ترین ================================================ FILE: i18n/fi/cosmic_files.ftl ================================================ cosmic-files = COSMICin tiedostot empty-folder = Tyhjä kansio empty-folder-hidden = Tyhjä kansio (sisältää piilotettuja kohteita) no-results = Ei tuloksia filesystem = Tiedostojärjestelmä home = Koti networks = Verkot notification-in-progress = Tiedostotoimintoja käynnissä trash = Roskakori recents = Viimeaikaiset undo = Kumoa today = Tänään # Desktop view options desktop-view-options = Työpöytänäkymän asetukset… show-on-desktop = Näytä työpöydällä desktop-folder-content = Työpöytäkansion sisältö mounted-drives = Liitetyt asemat trash-folder-icon = Roskakorikansion kuvake icon-size-and-spacing = Kuvakkeen koko ja välistys icon-size = Kuvakkeen koko # List view name = Nimi modified = Muokattu trashed-on = Siirretty roskakoriin size = Koko # Dialogs ## Compress Dialog create-archive = Luo arkisto ## Empty Trash Dialog empty-trash = Tyhjennä roskakori empty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi ## Mount Error Dialog mount-error = Levy on saavuttamattomissa ## New File/Folder Dialog create-new-file = Luo uusi tiedosto create-new-folder = Luo uusi kansio file-name = Tiedoston nimi folder-name = Kansion nimi file-already-exists = Tiedosto samalla nimellä on jo olemassa folder-already-exists = Kansio samalla nimellä on jo olemassa name-hidden = Merkillä "." alkavat nimet piilotetaan name-invalid = Nimi ei voi olla "{ $filename }" name-no-slashes = Nimi ei voi sisältää vinoviivoja ## Open/Save Dialog cancel = Peru create = Luo open = Avaa open-file = Avaa tiedosto open-folder = Avaa kansio open-in-new-tab = Avaa uudessa välilehdessä open-in-new-window = Avaa uudessa ikkunassa open-item-location = Avaa kohteen sijainti open-multiple-files = Avaa useita tiedostoja open-multiple-folders = Avaa useita kansioita save = Tallenna save-file = Tallenna tiedosto ## Open With Dialog open-with-title = Miten haluat avata kohteen "{ $name }"? browse-store = Selaa { $store }a ## Rename Dialog rename-file = Nimeä tiedosto uudelleen rename-folder = Nimeä kansio uudelleen ## Replace Dialog replace = Korvaa replace-title = "{ $filename }" on jo olemassa tässä sijainnissa replace-warning = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön. replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön. original-file = Alkuperäinen tiedosto replace-with = Korvaa käyttäen apply-to-all = Toteuta kaikkiin keep-both = Pidä molemmat skip = Ohita ## Set as Executable and Launch Dialog set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettäväksi ja käynnistää sen? set-and-launch = Aseta ja käynnistä ## Metadata Dialog owner = Omistaja group = Ryhmä other = Muut # Context Pages ## About ## Add Network Drive add-network-drive = Lisää verkkolevy connect = Yhdistä connect-anonymously = Yhdistä nimettömästi connecting = Yhdistetään… domain = Verkkotunnus 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] ### Make sure to keep the comma which separates the columns network-drive-schemes = Saatavilla olevat yhteyskäytännöt,Etuliite AppleTalk,afp:// File Transfer Protocol,ftp:// tai ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// tai ssh:// WebDav,dav:// tai davs:// network-drive-error = Verkkolevy ei saatavilla password = Salasana remember-password = Muista salasana try-again = Yritä uudelleen username = Käyttäjätunnus ## Operations edit-history = Muokkaa historiaa history = Historia no-history = Historia on tyhjä. pending = Jonossa failed = Epäonnistuneet complete = Valmiit compressing = Pakataan { $items } { $items -> [one] kohde *[other] kohdetta } sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })… compressed = Pakattu { $items } { $items -> [one] kohde *[other] kohdetta } sijainnista "{ $from }" arkistoon "{ $to }" copy_noun = Kopio creating = Luodaan "{ $name }" kohteen "{ $parent }" alle created = Luotu "{ $name }" kohteen "{ $parent }" alle copying = Kopioidaan { $items } { $items -> [one] kohde *[other] kohdetta } sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… copied = Kopioitu { $items } { $items -> [one] kohde *[other] kohdetta } sijainnista "{ $from }" kohteeseen "{ $to }" emptying-trash = Tyhjennetään { trash } ({ $progress })… emptied-trash = Tyhjennetty { trash } extracting = Puretaan { $items } { $items -> [one] kohde *[other] kohdetta } arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })… extracted = Purettu { $items } { $items -> [one] kohde *[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] kohdetta } sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… moved = Siirretty { $items } { $items -> [one] kohde *[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] kohdetta } roskakorista ({ $progress })… restored = Palautettu { $items } { $items -> [one] kohde *[other] kohdetta } roskakorista unknown-folder = tuntematon kansio ## Open with menu-open-with = Avaa sovelluksella… default-app = { $name } (oletus) ## Show details show-details = Näytä yksityiskohdat ## Settings settings = Asetukset ### Appearance appearance = Ulkoasu theme = Teema match-desktop = Sovita työpöytään dark = Tumma light = Vaalea # Context menu add-to-sidebar = Lisää sivupalkkiin compress = Pakkaa… extract-here = Pura new-file = Uusi tiedosto… new-folder = Uusi kansio… open-in-terminal = Avaa päätteessä move-to-trash = Siirrä roskakoriin restore-from-trash = Palauta roskakorista remove-from-sidebar = Poista sivupalkista sort-by-name = Järjestä nimen mukaan sort-by-modified = Järjestä muokkausajan mukaan sort-by-size = Järjestä koon mukaan sort-by-trashed = Järjestä poistamisajan mukaan ## Desktop change-wallpaper = Vaihda taustakuvaa… desktop-appearance = Työpöydän ulkoasu… display-settings = Näytön asetukset… # Menu ## File file = Tiedosto new-tab = Uusi välilehti new-window = Uusi ikkuna rename = Nimeä uudelleen… close-tab = Sulje välilehti quit = Lopeta ## Edit edit = Muokkaa cut = Leikkaa copy = Kopioi paste = Liitä select-all = Valitse kaikki ## View zoom-in = Lähennä default-size = Oletuskoko zoom-out = Loitonna view = Näytä grid-view = Ruudukkonäkymä list-view = Listanäkymä show-hidden-files = Näytä piilotetut tiedostot list-directories-first = Näytä kansiot ensin gallery-preview = Gallerian esikatselu menu-settings = Asetukset… menu-about = Tietoa COSMICin tiedostonhallinnasta… ## Sort sort = Järjestä sort-a-z = A-Ö sort-z-a = Ö-A sort-newest-first = Uusin ensin sort-oldest-first = Vanhin ensin sort-smallest-to-largest = Pienimmästä suurimpaan sort-largest-to-smallest = Suurimmasta pienimpään resume = Jatka extract-password-required = Salasana vaaditaan extract-to-title = Pura kansioon empty-trash-title = Tyhjennetäänkö roskakori? other-apps = Muut sovellukset related-apps = Liittyvät sovellukset permanently-delete-question = Poistetaanko pysyvästi? delete = Poista open-with = Avaa sovelluksella remove = Poista cancelled = Peruttu type = Tyyppi: { $mime } item-size = Koko: { $size } item-created = Luotu: { $created } item-modified = Muokattu: { $modified } delete-permanently = Poista pysyvästi reload-folder = Lataa kansio uudelleen comment = Tiedostonhallinta COSMIC-työpöydälle keywords = Folder;Manager;Kansio;Hakemisto;Hallinta;Hallinnointi;Hallitse;Hallinnoi; copy-to-button-label = Kopioi 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 ================================================ FILE: i18n/fr/cosmic_files.ftl ================================================ cosmic-files = Fichiers COSMIC empty-folder = Dossier vide empty-folder-hidden = Dossier vide (contient des éléments cachés) no-results = Aucun résultat trouvé filesystem = Système de fichiers home = Dossier personnel networks = Réseaux notification-in-progress = Des opérations sur des fichiers sont en cours trash = Corbeille recents = Récents undo = Annuler today = Aujourd'hui # Desktop view options desktop-view-options = Options d'affichage du bureau... show-on-desktop = Afficher sur le bureau desktop-folder-content = Contenu du dossier du bureau mounted-drives = Lecteurs montés trash-folder-icon = Icône du dossier Corbeille icon-size-and-spacing = Taille et espacement des icônes icon-size = Taille des icônes grid-spacing = Espacement de la grille # List view name = Nom modified = Modifié trashed-on = Mis à la corbeille size = Taille # Progress footer details = Détails dismiss = Ignorer le message operations-running = { $running } { $running -> [one] opération *[other] opérations } en cours ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] opération *[other] opérations } en cours ({ $percent }%), { $finished } terminé... pause = Pause resume = Reprendre # Dialogs ## Compress Dialog create-archive = Créer une archive ## Extract Dialog extract-password-required = Mot de passe requis extract-to = Extraire vers... extract-to-title = Extraire vers le dossier ## Empty Trash Dialog empty-trash = Vider la corbeille empty-trash-warning = Les éléments de la corbeille seront définitivement supprimés ## Mount Error Dialog mount-error = Impossible d'accéder au lecteur ## New File/Folder Dialog create-new-file = Créer un nouveau fichier create-new-folder = Créer un nouveau dossier file-name = Nom du fichier folder-name = Nom du dossier file-already-exists = Un fichier portant ce nom existe déjà folder-already-exists = Un dossier portant ce nom existe déjà name-hidden = Les noms commençant par "." seront cachés name-invalid = Le nom ne peut pas être "{ $filename }" name-no-slashes = Le nom ne peut pas contenir de slashs ## Open/Save Dialog cancel = Annuler create = Créer open = Ouvrir open-file = Ouvrir le fichier open-folder = Ouvrir dossier open-in-new-tab = Ouvrir dans un nouvel onglet open-in-new-window = Ouvrir dans une nouvelle fenêtre open-item-location = Ouvrir l'emplacement de l'élément open-multiple-files = Ouvrir plusieurs fichiers open-multiple-folders = Ouvrir plusieurs dossiers save = Enregistrer save-file = Enregistrer fichier ## Open With Dialog open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ? browse-store = Parcourir { $store } ## Permanently delete Dialog selected-items = Les { $items } éléments sélectionnés permanently-delete-question = Supprimer définitivement ? delete = Supprimer permanently-delete-warning = { $target } sera définitivement supprimé. Cette action ne peut pas être annulée. ## Rename Dialog rename-file = Renommer le fichier rename-folder = Renommer le dossier ## Replace Dialog replace = Remplacer replace-title = "{ $filename }" existe déjà à cet endroit replace-warning = Voulez-vous remplacer ce fichier par celui que vous enregistrez ? Cela écrasera son contenu. replace-warning-operation = Voulez-vous remplacer ce fichier ? Cela écrasera son contenu. original-file = Fichier d'origine replace-with = Remplacer par apply-to-all = Appliquer à tous keep-both = Conserver les deux skip = Ignorer ## Set as Executable and Launch Dialog set-executable-and-launch = Définir comme exécutable et lancer set-executable-and-launch-description = Voulez-vous définir "{ $name }" comme exécutable et le lancer ? set-and-launch = Définir et lancer ## Metadata Dialog open-with = Ouvrir avec owner = Propriétaire group = Groupe other = Autre ### Mode 0 none = Aucun ### Mode 1 (unusual) execute-only = Exécution seule ### Mode 2 (unusual) write-only = Écriture seule ### Mode 3 (unusual) write-execute = Écriture et exécution ### Mode 4 read-only = Lecture seule ### Mode 5 read-execute = Lecture et exécution ### Mode 6 read-write = Lecture et écriture ### Mode 7 read-write-execute = Lecture, écriture et exécution ## Favorite Path Error Dialog favorite-path-error = Erreur lors de l'ouverture du répertoire favorite-path-error-description = Impossible d'ouvrir "{ $path }" "{ $path }" n'existe peut-être pas ou vous n'avez pas la permission de l'ouvrir Voulez-vous le retirer de la barre latérale ? remove = Supprimer keep = Conserver # Context Pages ## About ## Add Network Drive add-network-drive = Ajouter un lecteur réseau connect = Connecter connect-anonymously = Se connecter anonymement connecting = Connexion... domain = Domaine enter-server-address = Entrez l'adresse du serveur network-drive-description = Les adresses de serveur incluent un préfixe de protocole et une adresse. Exemples : ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocoles disponibles,Préfixe AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDav,dav:// or davs:// network-drive-error = Impossible d'accéder au lecteur réseau password = Mot de passe remember-password = Se souvenir du mot de passe try-again = Essayer à nouveau username = Nom d'utilisateur ## Operations cancelled = Annulé edit-history = Modifier l'historique history = Historique no-history = Aucun élément dans l'historique. pending = En attente progress = { $percent }% progress-cancelled = { $percent }%, annulation progress-paused = { $percent }%, en pause failed = Échoué complete = Terminé compressing = Compression de { $items } { $items -> [one] élément *[other] éléments } depuis "{ $from }" vers "{ $to }" ({ $progress })... compressed = { $items } { $items -> [one] élément compressé *[other] éléments compressés } depuis "{ $from }" vers "{ $to }" copy_noun = Copier creating = Création de "{ $name }" dans "{ $parent }" created = "{ $name }" créé dans "{ $parent }" copying = Copie de { $items } { $items -> [one] élément *[other] éléments } depuis "{ $from }" vers "{ $to }" ({ $progress })... copied = { $items } { $items -> [one] élément copié *[other] éléments copiés } depuis "{ $from }" vers "{ $to }" deleting = Suppression de { $items } { $items -> [one] élément *[other] éléments } depuis { trash } ({ $progress })... deleted = { $items } { $items -> [one] élément supprimé *[other] éléments supprimés } depuis { trash } emptying-trash = { trash } en cours de nettoyage ({ $progress })... emptied-trash = { trash } vidée extracting = Extraction de { $items } { $items -> [one] élément *[other] éléments } depuis "{ $from }" vers "{ $to }" ({ $progress })... extracted = { $items } { $items -> [one] élément extrait *[other] éléments extraits } depuis "{ $from }" vers "{ $to }" setting-executable-and-launching = Paramétrage de "{ $name }" comme exécutable et prêt à être lancé set-executable-and-launched = Défini "{ $name }" comme exécutable et lancé moving = Déplacement de { $items } { $items -> [one] élément *[other] éléments } depuis "{ $from }" vers "{ $to }" ({ $progress })... moved = { $items } { $items -> [one] élément déplacé *[other] éléments déplacés } de "{ $from }" à "{ $to }" permanently-deleting = Suppression définitive de { $items } { $items -> [one] élément *[other] éléments } permanently-deleted = { $items } { $items -> [one] élément supprimé *[other] éléments supprimés } définitivement renaming = Renommage de "{ $from }" en "{ $to }" renamed = "{ $from }" renommé en "{ $to }" restoring = Restauration de { $items } { $items -> [one] élément *[other] éléments } depuis la { trash } ({ $progress })... restored = { $items } { $items -> [one] élément restauré *[other] éléments restaurés } depuis la { trash } unknown-folder = Dossier inconnu ## Open with menu-open-with = Ouvrir avec... default-app = { $name } (défaut) ## Show details show-details = Afficher les détails type = Type : { $mime } items = Éléments : { $items } item-size = Taille : { $size } item-created = Créé : { $created } item-modified = Modifié : { $modified } item-accessed = Consulté : { $accessed } calculating = Calcul en cours... ## Settings settings = Paramètres single-click = Ouvrir en un clic ### Appearance appearance = Apparence theme = Thème match-desktop = Assortir au bureau dark = Sombre light = Clair ### Type to Search type-to-search = Tapez pour rechercher type-to-search-recursive = Recherche dans le dossier actuel et tous les sous-dossiers type-to-search-enter-path = Entrez le chemin du dossier ou du fichier # Context menu add-to-sidebar = Ajouter à la barre latérale compress = Compresser... delete-permanently = Supprimer définitivement extract-here = Extraire new-file = Nouveau fichier... new-folder = Nouveau dossier... open-in-terminal = Ouvrir dans le terminal move-to-trash = Déplacer vers la corbeille restore-from-trash = Restaurer depuis la corbeille remove-from-sidebar = Supprimer de la barre latérale sort-by-name = Trier par nom sort-by-modified = Trier par date de modification sort-by-size = Trier par taille sort-by-trashed = Trier par date de suppression ## Desktop change-wallpaper = Changer le fond d'écran... desktop-appearance = Apparence du bureau... display-settings = Paramètres d'affichage... # Menu ## File file = Fichier new-tab = Nouvel onglet new-window = Nouvelle fenêtre rename = Renommer... close-tab = Fermer l'onglet quit = Quitter ## Edit edit = Modifier cut = Couper copy = Copier paste = Coller select-all = Sélectionner tout ## View zoom-in = Zoomer default-size = Taille par défaut zoom-out = Dézoomer view = Affichage grid-view = Vue en grille list-view = Vue en liste show-hidden-files = Afficher les fichiers cachés list-directories-first = Lister les répertoires en premier gallery-preview = Aperçu de la galerie menu-settings = Paramètres... menu-about = À propos de Fichiers COSMIC... ## Sort sort = Trier sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Le plus récent en premier sort-oldest-first = Le plus ancien en premier sort-smallest-to-largest = Du plus petit au plus grand sort-largest-to-smallest = Du plus grand au plus petit reload-folder = Actualiser le dossier related-apps = Applications associées removing-from-recents = Suppression de { $items } { $items -> [one] élément *[other] éléments } de { recents } other-apps = Autres applications set-permissions = Définir les permissions pour "{ $name }" à { $mode } repository = Dépôt support = Support eject = Éjecter remove-from-recents = Supprimer des récents empty-trash-title = Vider la corbeille ? setting-permissions = Définition des permissions pour "{ $name }" à { $mode } removed-from-recents = { $items } { $items -> [one] élément supprimé *[other] éléments supprimés } de { recents } progress-failed = { $percent }%, échec type-to-search-select = Sélectionne le premier fichier ou dossier qui convient pasted-image = Image collée pasted-text = Text collé pasted-video = Vidéo collée copy-to-title = Sélectionner la destination de la copie copy-to-button-label = Copier move-to-title = Sélectionner la destination du déplacement move-to-button-label = Déplacer copy-to = Copier vers... move-to = Déplacer vers... comment = Explorateur de fichiers pour le bureau COSMIC 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 ================================================ FILE: i18n/fy/cosmic_files.ftl ================================================ support = Stipe repository = Argyf open-file = Iepenje in bestân cancel = Annulearje open-folder = Iepenje in map settings = Ynstellings match-desktop = Systeemstandert cosmic-files = COSMIC Bestannen empty-folder = Lege map empty-folder-hidden = Lege map (mei ferburgen bestannen) no-results = Gjin resultaten fûn ================================================ FILE: i18n/ga/cosmic_files.ftl ================================================ cosmic-files = Comhaid COSMIC empty-folder = Fillteán folamh empty-folder-hidden = Fillteán folamh (tá míreanna folaithe ann) no-results = Níor aimsíodh aon torthaí filesystem = Córas comhad home = Baile networks = Líonraí notification-in-progress = Tá oibríochtaí comhaid ar siúl trash = Bruscar recents = Le Déanaí undo = Cuir ar ceal today = Inniu # Desktop view options desktop-view-options = Roghanna radhairc deisce... show-on-desktop = Taispeáin ar an deasc desktop-folder-content = Ábhar fillteáin deisce mounted-drives = Tiomántáin mhonaithe trash-folder-icon = Deilbhín fillteáin bruscair icon-size-and-spacing = Méid agus spásáil na ndeilbhíní icon-size = Méid na ndeilbhíní grid-spacing = Spásáil an ghreille # List view name = Ainm modified = Mionathraithe trashed-on = Curtha sa bhruscar size = Méid # Progress footer details = Sonraí dismiss = Diúltaigh an teachtaireacht operations-running = { $running } { $running -> [one] oibríocht *[other] oibríochtaí } ag rith ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] oibríocht *[other] oibríochtaí } ag rith ({ $percent }%), { $finished } críochnaithe... pause = Sos resume = Tosaigh arís # Dialogs ## Compress Dialog create-archive = Cruthaigh cartlann ## Extract Dialog extract-password-required = Pasfhocal riachtanach extract-to = Asbhain go... extract-to-title = Asbhain go fillteán ## Empty Trash Dialog empty-trash = Folmhaigh an bruscar empty-trash-warning = Scriosfar míreanna sa bhfillteán Bruscair go buan ## Mount Error Dialog mount-error = Ní féidir rochtain a fháil ar an tiomántán ## New File/Folder Dialog create-new-file = Cruthaigh comhad nua create-new-folder = Cruthaigh fillteán nua file-name = Ainm comhaid folder-name = Ainm fillteáin file-already-exists = Tá comhad leis an ainm sin ann cheana féin folder-already-exists = Tá fillteán leis an ainm sin ann cheana féin name-hidden = Beidh ainmneacha ag tosú le "." i bhfolach name-invalid = Ní féidir an t-ainm a bheith "{ $filename }" name-no-slashes = Ní féidir slaiseanna a bheith san ainm ## Open/Save Dialog cancel = Cealaigh create = Cruthaigh open = Oscail open-file = Oscail comhad open-folder = Oscail fillteán open-in-new-tab = Oscail i gcluaisín nua open-in-new-window = Oscail i bhfuinneog nua open-item-location = Oscail suíomh na míre open-multiple-files = Oscail ilchomhaid open-multiple-folders = Oscail ilfhillteáin save = Sábháil save-file = Sábháil comhad ## Open With Dialog open-with-title = Conas is mian leat "{ $name }" a oscailt? browse-store = Brabhsáil { $store } ## Rename Dialog rename-file = Athainmnigh comhad rename-folder = Athainmnigh fillteán ## Replace Dialog replace = Cuir in ionad replace-title = Tá "{ $filename }" ann sa suíomh seo cheana féin replace-warning = An bhfuil tú cinnte gur mian leat é a chur in ionad leis an gceann atá á shábháil agat? Scríobhfar an t-ábhar nua thairis ar an ábhar atá ann cheana. replace-warning-operation = An bhfuil tú cinnte gur mian leat é a chur in ionad? Scríobhfar an t-ábhar nua thairis ar an ábhar atá ann cheana. original-file = Comhad bunaidh replace-with = Cuir in ionad le apply-to-all = Cuir i bhfeidhm ar gach ceann keep-both = Coinnigh an dá cheann skip = Scipeáil ## Set as Executable and Launch Dialog set-executable-and-launch = Socraigh mar inrite agus lainseáil set-executable-and-launch-description = Ar mhaith leat "{ $name }" a shocrú mar chomhad inrite agus é a lainseáil? set-and-launch = Socraigh agus lainseáil ## Metadata Dialog open-with = Oscail le owner = Úinéir group = Grúpa other = Eile ### Mode 0 none = Dada ### Mode 1 (unusual) execute-only = Inrite amháin ### Mode 2 (unusual) write-only = Scríobh amháin ### Mode 3 (unusual) write-execute = Scríobh agus inrite ### Mode 4 read-only = Léamh amháin ### Mode 5 read-execute = Léamh agus inrite ### Mode 6 read-write = Léamh agus scríobh ### Mode 7 read-write-execute = Léamh, scríobh, agus inrite ## Favorite Path Error Dialog favorite-path-error = Earráid ag oscailt an eolaire favorite-path-error-description = Ní féidir "{ $path }" a oscailt B'fhéidir nach bhfuil "{ $path }" ann nó b'fhéidir nach bhfuil cead agat é a oscailt Ar mhaith leat é a bhaint den bharra taoibh? remove = Bain keep = Coimeád # Context Pages ## About ## Add Network Drive add-network-drive = Cuir tiomántán líonra leis connect = Ceangail connect-anonymously = Ceangail gan ainm connecting = Ag ceangal... domain = Fearann enter-server-address = Cuir isteach seoladh an fhreastalaí network-drive-description = Áirítear le seoltaí freastalaí réimír prótacail agus seoladh. Samplaí: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Prótacail atá ar fáil, Réimír AppleTalk,afp:// Prótacal Aistrithe Comhad,ftp:// nó ftps:// Córas Comhad Líonra,nfs:// Bloc Teachtaireachtaí Freastalaí,smb:// Prótacal Aistrithe Comhad SSH,sftp:// nó ssh:// WebDav,dav:// nó davs:// network-drive-error = Ní féidir teacht ar an tiomántán líonra password = Pasfhocal remember-password = Cuimhnigh pasfhocal try-again = Bain triail eile as username = Ainm úsáideora ## Operations cancelled = Cealaithe edit-history = Cuir stair in eagar history = Stair no-history = Gan aon mhíreanna sa stair. pending = Ar feitheamh progress = { $percent }% progress-cancelled = { $percent }%, cealaithe progress-paused = Curtha ar shos ag { $percent }% failed = Theip complete = Críochnaithe compressing = Á chomhbhrú { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" ({ $progress })... compressed = Comhbhrúdh { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" copy_noun = Cóipeáil creating = Á chruthú "{ $name }" i "{ $parent }" created = Cruthaíodh "{ $name }" i "{ $parent }" copying = Ag cóipeáil { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" ({ $progress })... copied = Cóipeáilte { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" deleting = Ag scriosadh { $items } { $items -> [one] mhír *[other] míreanna } ó { trash } ({ $progress })... deleted = Scriosta { $items } { $items -> [one] mhír *[other] míreanna } ó { trash } emptying-trash = Ag folmhú { trash } ({ $progress })... emptied-trash = Folmhaíodh an { trash } extracting = Ag asbhaint { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" ({ $progress })... extracted = Asbhainte { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" setting-executable-and-launching = Ag socrú "{ $name }" mar inrite agus ag lainseáil set-executable-and-launched = Socraigh "{ $name }" mar inrite agus lainseáilte moving = Ag bogadh { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" ({ $progress })... moved = Bogadh { $items } { $items -> [one] mhír *[other] míreanna } ó "{ $from }" go "{ $to }" renaming = Ag athainmniú "{ $from }" go "{ $to }" renamed = Athainmnithe "{ $from }" go "{ $to }" restoring = Ag athchóiriú{ $items } { $items -> [one] mhír *[other] míreanna } ó { trash } ({ $progress })... restored = Athchóirithe { $items } { $items -> [one] mhír *[other] míreanna } ó { trash } unknown-folder = Fillteán anaithnid ## Open with menu-open-with = Oscail le... default-app = { $name } (réamhshocraithe) ## Show details show-details = Taispeáin sonraí type = Cineál: { $mime } items = Míreanna: { $items } item-size = Méid: { $size } item-created = Cruthaithe: { $created } item-modified = Mionathraithe: { $modified } item-accessed = Rochtainte: { $accessed } calculating = Á ríomh... ## Settings settings = Socruithe single-click = Cliceáil amháin le hoscailt ### Appearance appearance = Cuma theme = Téama match-desktop = Meaitseáil deasc dark = Dorcha light = Solas ### Type to Search type-to-search = Clóscríobh le cuardach type-to-search-recursive = Cuardaíonn sé an fillteán reatha agus na fo-fhillteáin go léir type-to-search-enter-path = Iontrálann sé seo an cosán chuig an eolaire nó an comhad # Context menu add-to-sidebar = Cuir leis an mbarra taoibh compress = Comhbhrúigh... delete-permanently = Scrios go buan extract-here = Asbhain new-file = Comhad nua... new-folder = Fillteán nua... open-in-terminal = Oscail sa teirminéal move-to-trash = Bog go dtí an bruscar restore-from-trash = Athchóirigh ón mbruscar remove-from-sidebar = Bain ón mbarra taoibh sort-by-name = Sórtáil de réir ainm sort-by-modified = Sórtáil de réir modhnaithe sort-by-size = Sórtáil de réir méid sort-by-trashed = Sórtáil de réir am scriosta ## Desktop change-wallpaper = Athraigh cúlbhrat... desktop-appearance = Cuma deisce... display-settings = Socruithe taispeána... # Menu ## File file = Comhad new-tab = Cluaisín nua new-window = Fuinneog nua rename = Athainmnigh... close-tab = Dún cluaisín quit = Scoir ## Edit edit = Eagar cut = Gearr copy = Cóipeáil paste = Greamaigh select-all = Roghnaigh gach ceann ## View zoom-in = Súmáil isteach default-size = Méid réamhshocraithe zoom-out = Súmáil amach view = Amharc grid-view = Amharc greille list-view = Amharc liosta show-hidden-files = Taispeáin comhaid fholaithe list-directories-first = Liostaigh eolairí ar dtús gallery-preview = Réamhamharc gailearaí menu-settings = Socruithe... menu-about = Maidir le Comhaid COSMIC... ## Sort sort = Sórtáil sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Is nuaí ar dtús sort-oldest-first = Is sine ar dtús sort-smallest-to-largest = Ón gceann is lú go dtí an ceann is mó sort-largest-to-smallest = Ón gceann is mó go dtí an ceann is lú repository = Stór support = Tacaíocht other-apps = Feidhmchláir eile related-apps = Feidhmchláir ghaolmhara selected-items = Na { $items } míreanna roghnaithe permanently-delete-question = Scriosadh go buan? delete = Scrios permanently-delete-warning = Scriosfar { $target } go buan. Ní féidir an gníomh seo a chealú. progress-failed = Theip ar { $percent }% setting-permissions = Ceadanna á socrú do "{ $name }" go { $mode } set-permissions = Socraigh ceadanna do "{ $name }" go { $mode } permanently-deleting = Ag scriosadh go buan { $items } { $items -> [one] mhír *[other] míreanna } permanently-deleted = Scriosta go buan { $items } { $items -> [one] mhír amháin *[other] míreanna } removing-from-recents = Ag baint { $items } { $items -> [one] mhír *[other] míreanna } ó { recents } removed-from-recents = Bainte { $items } { $items -> [one] mhír *[other] míreanna } ó { recents } eject = Díchuir remove-from-recents = Bain as na cinn is déanaí reload-folder = Athlódáil an fillteán empty-trash-title = Folmhaigh an bruscar? type-to-search-select = Roghnaíonn an chéad chomhad nó fillteán comhoiriúnach pasted-image = Íomhá ghreamaithe pasted-text = Téacs greamaithe pasted-video = Físeán greamaithe copy-to-title = Roghnaigh ceann scríbe cóipeála copy-to-button-label = Cóipeáil move-to-title = Roghnaigh ceann scríbe an bhogtha move-to-button-label = Bog copy-to = Cóipeáil chuig... move-to = Bog go... comment = Bainisteoir comhad don deasc COSMIC 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 ================================================ FILE: i18n/gd/cosmic_files.ftl ================================================ ================================================ FILE: i18n/gu/cosmic_files.ftl ================================================ ================================================ FILE: i18n/he/cosmic_files.ftl ================================================ connect = התחברות progress = { $percent }% ================================================ FILE: i18n/hi/cosmic_files.ftl ================================================ cosmic-files = कॉस्मिक फाइल्स empty-folder = खाली फ़ोल्डर empty-folder-hidden = खाली फ़ोल्डर (अदृश्य आइटम शामिल हैं) no-results = कोई परिणाम नहीं filesystem = फाइल सिस्टम home = होम networks = नेटवर्क्स notification-in-progress = फाइल संचालन प्रगति पर है। trash = कचरा recents = हाल के undo = पूर्ववत करें today = आज # Desktop view options desktop-view-options = डेस्कटॉप दृश्य विकल्प... show-on-desktop = डेस्कटॉप पर दिखाएं desktop-folder-content = डेस्कटॉप फ़ोल्डर सामग्री mounted-drives = माउंट किए गए ड्राइव trash-folder-icon = कचरा फ़ोल्डर आइकन icon-size-and-spacing = आइकन आकार और अंतर icon-size = आइकन आकार # List view name = नाम modified = संशोधित तिथि trashed-on = कचरे में डालने की तिथि size = आकार # Dialogs ## Compress Dialog create-archive = संग्रह बनाएँ ## Empty Trash Dialog empty-trash = कचरा खाली करें empty-trash-warning = क्या आप वाकई कचरे में सभी आइटमों को स्थायी रूप से हटाना चाहते हैं? ## New File/Folder Dialog create-new-file = नई फाइल बनाएँ create-new-folder = नया फ़ोल्डर बनाएँ file-name = फाइल का नाम folder-name = फ़ोल्डर का नाम file-already-exists = इसी नाम की फाइल पहले से मौजूद है। folder-already-exists = इसी नाम का फ़ोल्डर पहले से मौजूद है। name-hidden = "." से शुरू होने वाले नाम छिपे रहेंगे। name-invalid = नाम "{ $filename }" मान्य नहीं है। name-no-slashes = नाम में स्लैश का उपयोग नहीं किया जा सकता है। ## Open/Save Dialog cancel = रद्द करें create = बनाएँ open = खोलें open-file = फ़ाइल खोलें open-folder = फ़ोल्डर खोलें open-in-new-tab = नई टैब में खोलें open-in-new-window = नई विंडो में खोलें open-item-location = आइटम का स्थान खोलें open-multiple-files = कई फ़ाइलें खोलें open-multiple-folders = कई फ़ोल्डर खोलें save = सहेजें save-file = फ़ाइल सहेजें ## Open With Dialog open-with-title = "{ $name }" को कैसे खोलना चाहेंगे? browse-store = { $store } में ब्राउज़ करें ## Rename Dialog rename-file = फाइल का नाम बदलें rename-folder = फ़ोल्डर का नाम बदलें ## Replace Dialog replace = प्रतिस्थापित करें replace-title = { $filename } पहले से इस स्थान पर मौजूद है। replace-warning = क्या आप इसे प्रतिस्थापित करना चाहते हैं? यदि प्रतिस्थापित किया गया, तो मौजूदा फ़ाइल को ओवरराइट किया जाएगा। replace-warning-operation = क्या आप इसे बदलना चाहते हैं? प्रतिस्थापित करने पर मौजूदा फ़ाइल ओवरराइट हो जाएगी। original-file = मूल फ़ाइल replace-with = इसके साथ प्रतिस्थापित करें apply-to-all = सभी पर लागू करें keep-both = दोनों रखें skip = छोड़ें ## Set as Executable and Launch Dialog set-executable-and-launch = निष्पादन योग्य के रूप में सेट करें और लॉन्च करें set-executable-and-launch-description = क्या आप निष्पादन योग्य के रूप में "{ $name }" सेट करना चाहते हैं और इसे लॉन्च करते हैं? set-and-launch = सेट करें और लॉन्च करें ## Metadata Dialog owner = मालिक group = समूह other = अन्य # Context Pages ## About ## Add Network Drive add-network-drive = नेटवर्क ड्राइव जोड़ें connect = कनेक्ट करें connect-anonymously = गुमनाम रूप से कनेक्ट करें connecting = कनेक्ट हो रहा है... domain = डोमेन enter-server-address = सर्वर का पता दर्ज करें network-drive-description = सर्वर पते प्रोटोकॉल उपसर्ग और पते सहित होते हैं। उदाहरण: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = उपलब्ध प्रोटोकॉल,उपसर्ग एप्पलटॉक,afp:// फ़ाइल ट्रांसफ़र प्रोटोकॉल,ftp:// या ftps:// नेटवर्क फ़ाइल सिस्टम,nfs:// सर्वर संदेश ब्लॉक,smb:// SSH फ़ाइल ट्रांसफ़र प्रोटोकॉल,sftp:// या ssh:// वेबDAV,dav:// या davs:// network-drive-error = नेटवर्क ड्राइव तक पहुंचने में असमर्थ password = पासवर्ड remember-password = पासवर्ड याद रखें try-again = फिर से कोशिश करें username = उपयोगकर्ता नाम ## Operations edit-history = संपादन इतिहास history = इतिहास no-history = इतिहास में कोई आइटम नहीं pending = लंबित failed = विफल complete = पूर्ण compressing = संपीड़ित किया जा रहा है { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } compressed = संपीड़ित किया गया { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } copy_noun = नकल creating = { $parent } में { $name } बनाया जा रहा है created = { $parent } में { $name } बनाया गया copying = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } नकल की जा रही है copied = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } नकल की गई emptying-trash = कचरा खाली किया जा रहा है emptied-trash = कचरा खाली किया गया extracting = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } निकाला जा रहा है extracted = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } निकाला गया setting-executable-and-launching = "{ $name }" को कार्यान्वयन के रूप में सेट किया जा रहा है और लॉन्च किया जा रहा है set-executable-and-launched = "{ $name }" को कार्यान्वयन के रूप में सेट किया गया है और लॉन्च किया गया है moving = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } स्थानांतरित किया जा रहा है moved = { $items } { $items -> [one] आइटम *[other] आइटम } से { $from } तक { $to } स्थानांतरित किया गया renaming = { $from } से { $to } तक नाम बदला जा रहा है renamed = { $from } से { $to } तक नाम बदला गया restoring = { $items } { $items -> [one] आइटम *[other] आइटम } को कचरे से पुनर्स्थापित किया जा रहा है restored = { $items } { $items -> [one] आइटम *[other] आइटम } को कचरे से पुनर्स्थापित किया गया unknown-folder = अज्ञात फ़ोल्डर ## Open with menu-open-with = इसके साथ खोलें default-app = { $name } (डिफ़ॉल्ट) ## Show details show-details = विवरण दिखाएँ ## Settings settings = सेटिंग ### Appearance appearance = रूप theme = थीम match-desktop = सिस्टम सेटिंग्स से मेल खाएँ dark = डार्क light = लाइट # Context menu add-to-sidebar = साइडबार में जोड़ें compress = संपीड़ित करें extract-here = यहाँ निकालें new-file = नई फ़ाइल... new-folder = नया फ़ोल्डर... open-in-terminal = टर्मिनल में खोलें move-to-trash = कचरे में भेजें restore-from-trash = कचरे से पुनर्स्थापित करें remove-from-sidebar = साइडबार से निकालें sort-by-name = नाम से क्रमबद्ध करें sort-by-modified = संशोधित तिथि द्वारा क्रमबद्ध करें sort-by-size = आकार द्वारा क्रमबद्ध करें sort-by-trashed = कचरे में डालने की तिथि द्वारा क्रमबद्ध करें ## Desktop change-wallpaper = वॉलपेपर बदलें... desktop-appearance = डेस्कटॉप रूप... display-settings = डिस्प्ले सेटिंग्स... # Menu ## File file = फ़ाइल new-tab = नया टैब new-window = नई विंडो rename = नाम बदलें... close-tab = टैब बंद करें quit = बंद करें ## Edit edit = संपादित करें cut = कट करें copy = नकल करें paste = चिपकाएँ select-all = सभी चुनें ## View zoom-in = ज़ूम इन करें default-size = डिफ़ॉल्ट आकार zoom-out = ज़ूम आउट करें view = दृश्य grid-view = ग्रिड दृश्य list-view = सूची दृश्य show-hidden-files = छिपी हुई फाइलें दिखाएँ list-directories-first = सबसे पहले डाइरेक्ट्री दिखाएँ menu-settings = सेटिंग्स... menu-about = कॉस्मिक फाइल्स के बारे में... ## Sort sort = क्रमबद्ध करें sort-a-z = अ-ह क्रम में क्रमबद्ध करें sort-z-a = ह-अ क्रम में क्रमबद्ध करें sort-newest-first = नए से पुराने sort-oldest-first = पुराने से नए sort-smallest-to-largest = छोटे से बड़े sort-largest-to-smallest = बड़े से छोटे repository = रिपॉजिटरी support = सहायता ================================================ FILE: i18n/hr/cosmic_files.ftl ================================================ ================================================ FILE: i18n/hu/cosmic_files.ftl ================================================ cosmic-files = COSMIC Fájlok comment = Fájlkezelő a COSMIC asztali környezethez keywords = mappa;fájl;kezelő; empty-folder = Üres mappa empty-folder-hidden = Üres mappa (Rejtett elemeket tartalmaz) no-results = Nincs találat filesystem = Fájlrendszer home = Saját mappa networks = Hálózatok notification-in-progress = A fájlműveletek folyamatban vannak trash = Kuka recents = Legutóbbiak undo = Visszavonás today = Ma # Desktop view options desktop-view-options = Asztali nézet beállításai… show-on-desktop = Megjelenítés az asztalon desktop-folder-content = Asztal mappa tartalma mounted-drives = Csatolt meghajtók trash-folder-icon = Kuka ikonja icon-size-and-spacing = Ikonméret és távolság icon-size = Ikonméret grid-spacing = Rácsköz # List view name = Név modified = Módosítva trashed-on = Kukába helyezve size = Méret # Progress footer details = Részletek dismiss = Üzenet bezárása operations-running = { $running } { $running -> [one] művelet *[other] művelet } fut ({ $percent }%)… operations-running-finished = { $running } { $running -> [one] művelet *[other] művelet } fut ({ $percent }%), { $finished } befejeződött… pause = Szünet resume = Folytatás # Dialogs ## Compress Dialog create-archive = Tömörített fájl létrehozása ## Extract Dialog extract-password-required = Jelszó szükséges extract-to = Kibontás ide… extract-to-title = Kibontás mappába ## Empty Trash Dialog empty-trash = Kuka ürítése empty-trash-warning = A kukában lévő összes elem véglegesen törölve lesz ## Mount Error Dialog mount-error = Nem érhető el a meghajtó ## New File/Folder Dialog create-new-file = Új fájl létrehozása create-new-folder = Új mappa létrehozása file-name = Fájlnév folder-name = Mappa neve file-already-exists = Már létezik ilyen nevű fájl folder-already-exists = Már létezik ilyen nevű mappa name-hidden = A ponttal kezdődő nevek rejtve lesznek name-invalid = A név nem lehet „{ $filename }” name-no-slashes = A név nem tartalmazhat „/” jelet ## Open/Save Dialog cancel = Mégse create = Létrehozás open = Megnyitás open-file = Fájl megnyitása open-folder = Mappa megnyitása open-in-new-tab = Megnyitás új lapon open-in-new-window = Megnyitás új ablakban open-item-location = Útvonal megnyitása open-multiple-files = Több fájl megnyitása open-multiple-folders = Több mappa megnyitása save = Mentés save-file = Fájl mentése ## Open With Dialog open-with-title = Hogyan szeretnéd megnyitni a következőt: „{ $name }”? browse-store = { $store } böngészése other-apps = Egyéb alkalmazások related-apps = Hasonló alkalmazások ## Permanently delete Dialog selected-items = A(z) { $items } kijelölt elem permanently-delete-question = Végleges törlés? delete = Törlés permanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem visszavonható. ## Rename Dialog rename-file = Fájl átnevezése rename-folder = Mappa átnevezése ## Replace Dialog replace = Csere replace-title = „{ $filename }” már létezik replace-warning = Szeretnéd lecserélni a meglévő fájlt a mentendő fájllal? A cserével felülírod annak tartalmát. replace-warning-operation = Szeretnéd lecserélni? A csere felülírja annak tartalmát. original-file = Eredeti fájl replace-with = Csere erre apply-to-all = Alkalmazás mindegyikre keep-both = Mindkettő megtartása skip = Kihagyás ## Set as Executable and Launch Dialog set-executable-and-launch = Végrehajthatóvá tétel és indítás set-executable-and-launch-description = Szeretnéd végrehajthatóvá tenni a(z) „{ $name }” fájlt és elindítani? set-and-launch = Beállítás és indítás ## Metadata Dialog open-with = Megnyitás ezzel owner = Tulajdonos group = Csoport other = Többi ### Mode 0 none = Nincs ### Mode 1 (unusual) execute-only = Csak végrehajtás ### Mode 2 (unusual) write-only = Csak írás ### Mode 3 (unusual) write-execute = Írás és végrehajtás ### Mode 4 read-only = Csak olvasás ### Mode 5 read-execute = Olvasás és végrehajtás ### Mode 6 read-write = Olvasás és írás ### Mode 7 read-write-execute = Olvasás, írás és végrehajtás ## Favorite Path Error Dialog favorite-path-error = Hiba a könyvtár megnyitásakor favorite-path-error-description = Nem sikerült megnyitni ezt: „{ $path }” „{ $path }” lehet nem létezik, vagy nincs jogosultságod a megnyitásához Szeretnéd eltávolítani az oldalsávról? remove = Eltávolítás keep = Megtartás # Context Pages ## About ## Add Network Drive add-network-drive = Hálózati meghajtó hozzáadása connect = Csatlakozás connect-anonymously = Csatlakozás névtelenül connecting = Csatlakozás… domain = Tartomány enter-server-address = Add meg a kiszolgáló címét network-drive-description = A kiszolgálócímek tartalmazzák a protokoll előtagját és a címet. Például: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Elérhető protokollok,Előtag AppleTalk,afp:// File Transfer Protocol,ftp:// vagy ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// vagy ssh:// WebDAV,dav:// vagy davs:// network-drive-error = Nem érhető el a hálózati meghajtó password = Jelszó remember-password = Jelszó megjegyzése try-again = Újra username = Felhasználónév ## Operations cancelled = Megszakítva edit-history = Fájlműveleti előzmények history = Előzmények no-history = Nem találhatók elemek az előzményekben pending = Függőben progress = { $percent }% progress-cancelled = { $percent }%, megszakítva progress-paused = { $percent }%, szüneteltetve failed = Sikertelen complete = Befejezett compressing = { $items } { $items -> [one] elem *[other] elem } tömörítése innen: „{ $from }” ide: „{ $to }” ({ $progress })… compressed = { $items } { $items -> [one] elem *[other] elem } tömörítve innen: „{ $from }” ide: „{ $to }” copy_noun = Másolat creating = „{ $name }” létrehozása itt: „{ $parent }” created = „{ $name }” létrehozva itt: „{ $parent }” copying = { $items } { $items -> [one] elem *[other] elem } másolása innen: „{ $from }” ide: „{ $to }” ({ $progress })… copied = { $items } { $items -> [one] elem *[other] elem } másolva innen: „{ $from }” ide: „{ $to }” deleting = { $items } { $items -> [one] elem *[other] elem } törlése a kukából ({ $progress })… deleted = { $items } { $items -> [one] elem *[other] elem } törölve a kukából emptying-trash = { trash } kiürítése ({ $progress })… emptied-trash = { trash } kiürítve extracting = { $items } { $items -> [one] elem *[other] elem } kibontása innen: „{ $from }” ide: „{ $to }” ({ $progress })… extracted = { $items } { $items -> [one] elem *[other] elem } kibontva innen: „{ $from }” ide: „{ $to }” setting-executable-and-launching = „{ $name }” végrehajthatóvá tétele és futtatása set-executable-and-launched = „{ $name }” végrehajthatóvá lett téve és futtatva setting-permissions = „{ $name }” jogosultságainak beállítása: { $mode } set-permissions = „{ $name }” jogosultságai beállítva: { $mode } moving = { $items } { $items -> [one] elem *[other] elem } áthelyezése innen: „{ $from }” ide: „{ $to }” ({ $progress })… moved = { $items } { $items -> [one] elem *[other] elem } áthelyezve innen: „{ $from }” ide: „{ $to }” permanently-deleting = { $items } { $items -> [one] elem *[other] elem } végleges törlése permanently-deleted = { $items } { $items -> [one] elem *[other] elem } véglegesen törölve removing-from-recents = { $items } { $items -> [one] elem *[other] elem } eltávolítása a { recents }ból removed-from-recents = { $items } { $items -> [one] elem *[other] elem } eltávolítva a { recents }ból renaming = „{ $from }” átnevezése erre: „{ $to }” renamed = „{ $from }” átnevezve erre: „{ $to }” restoring = { $items } { $items -> [one] elem *[other] elem } visszaállítása a kukából ({ $progress })… restored = { $items } { $items -> [one] elem *[other] elem } visszaállítva a kukából unknown-folder = ismeretlen mappa ## Open with menu-open-with = Megnyitás mással… default-app = { $name } (alapértelmezett) ## Show details show-details = Részletek megjelenítése type = Típus: { $mime } items = Elemek: { $items } item-size = Méret: { $size } item-created = Létrehozva: { $created } item-modified = Módosítva: { $modified } item-accessed = Hozzáférve: { $accessed } calculating = Számítás… ## Settings settings = Beállítások single-click = Egykattintásos megnyitás ### Appearance appearance = Megjelenés theme = Téma match-desktop = Rendszertéma dark = Sötét light = Világos ### Type to Search type-to-search = Gépeléssel keresés type-to-search-recursive = Keresés a jelenlegi mappában és almappákban type-to-search-enter-path = Elérési út megadása # Context menu add-to-sidebar = Hozzáadás az oldalsávhoz compress = Tömörítés… delete-permanently = Végleges törlés eject = Kiadás extract-here = Kibontás new-file = Új fájl… new-folder = Új mappa… open-in-terminal = Megnyitás a terminálban move-to-trash = Kukába helyezés restore-from-trash = Visszaállítás a kukából remove-from-sidebar = Eltávolítás az oldalsávról sort-by-name = Név szerinti rendezés sort-by-modified = Módosítás szerinti rendezés sort-by-size = Méret szerinti rendezés sort-by-trashed = Törlés ideje szerinti rendezés remove-from-recents = Eltávolítás a legutóbbiak közül ## Desktop change-wallpaper = Háttérkép cseréje… desktop-appearance = Asztali megjelenés… display-settings = Képernyő-beállítások… # Menu ## File file = Fájl new-tab = Új lap new-window = Új ablak reload-folder = Mappa újratöltése rename = Átnevezés… close-tab = Lap bezárása quit = Kilépés ## Edit edit = Szerkesztés cut = Kivágás copy = Másolás paste = Beillesztés select-all = Összes kijelölése ## View zoom-in = Nagyítás default-size = Alapértelmezett méret zoom-out = Kicsinyítés view = Nézet grid-view = Rácsnézet list-view = Listanézet show-hidden-files = Rejtett fájlok megjelenítése list-directories-first = Könyvtárak listázása először gallery-preview = Galéria előnézet menu-settings = Beállítások… menu-about = A COSMIC Fájlok névjegye… ## Sort sort = Rendezés sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Legújabb előre sort-oldest-first = Legrégibb előre sort-smallest-to-largest = Legkisebbtől a legnagyobbig sort-largest-to-smallest = Legnagyobbtól a legkisebbig repository = Tároló support = Támogatás progress-failed = { $percent }%, sikertelen empty-trash-title = Kuka ürítése? type-to-search-select = Kijelöli az első egyező fájlt vagy mappát pasted-image = Beillesztett kép pasted-text = Beillesztett szöveg pasted-video = Beillesztett videó copy-to-title = Másolási cél kiválasztása copy-to-button-label = Másolás move-to-title = Áthelyezési cél kiválasztása move-to-button-label = Áthelyezés copy-to = Másolás ide… 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 ================================================ FILE: i18n/id/cosmic_files.ftl ================================================ empty-folder = Map kosong empty-folder-hidden = Map kosong (memiliki item tersembunyi) no-results = Tidak ada hasil yang ditemukan filesystem = Sistem berkas cosmic-files = Berkas COSMIC home = Beranda networks = Jaringan notification-in-progress = Operasi berkas sedang berlangsung trash = Sampah recents = Terbaru undo = Batalkan today = Hari ini desktop-view-options = Opsi tampilan desktop... show-on-desktop = Tampilkan di Desktop desktop-folder-content = Konten map desktop mounted-drives = Drive terpasang trash-folder-icon = Ikon map sampah icon-size-and-spacing = Ukuran dan jarak ikon icon-size = Ukuran ikon name = Nama grid-spacing = Jarak antar kisi modified = Dimodifikasi trashed-on = Dibuang size = Ukuran details = Rincian dismiss = Abaikan pesan operations-running = { $running } { $running -> [one] operasi *[other] operasi } berjalan ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operasi *[other] operasi } berjalan ({ $percent }%), { $finished } selesai... pause = Jeda resume = Lanjutkan create-archive = Buat arsip extract-password-required = Kata sandi diperlukan extract-to = Ekstrak ke... extract-to-title = Ekstrak ke map empty-trash = Kosongkan sampah empty-trash-title = Kosongkan sampah? empty-trash-warning = Item di map Sampah akan dihapus secara permanen emptying-trash = Mengosongkan { trash } ({ $progress })... mount-error = Tidak dapat mengakses drive create-new-file = Buat berkas baru create-new-folder = Buat map baru permanently-delete-question = Hapus secara permanen? delete = Hapus sort-by-trashed = Urutkan berdasarkan waktu penghapusan delete-permanently = Hapus secara permanen permanently-delete-warning = { $target } akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan. deleted = { $items } { $items -> [one] item *[other] item } dihapus dari { trash } permanently-deleted = { $items } { $items -> [one] item *[other] item } dihapus secara permanen file-name = Nama berkas folder-name = Nama map file-already-exists = Berkas dengan nama tersebut sudah ada folder-already-exists = Map dengan nama tersebut sudah ada name-hidden = Nama yang diawali dengan "." akan disembunyikan name-invalid = Nama tidak boleh "{ $filename }" name-no-slashes = Nama tidak boleh berisi garis miring cancel = Batalkan create = Buat open = Buka open-file = Buka berkas open-folder = Buka map open-in-new-tab = Buka di tab baru open-in-new-window = Buka di jendela baru open-item-location = Buka lokasi item open-multiple-files = Buka beberapa berkas open-multiple-folders = Buka beberapa map save = Simpan save-file = Simpan berkas open-with-title = Bagaimana anda ingin membuka "{ $name }"? browse-store = Telusuri { $store } other-apps = Aplikasi lainnya related-apps = Aplikasi terkait rename-file = Ganti nama berkas rename-folder = Ganti nama map replace = Ganti replace-title = "{ $filename }" sudah ada di lokasi ini replace-warning-operation = Apakah anda ingin menggantinya? Menggantinya akan menimpa konten tersebut. original-file = Berkas asli replace-with = Ganti dengan apply-to-all = Terapkan ke semua replace-warning = Apakah anda ingin menggantinya dengan yang sedang anda simpan? Menggantinya akan menimpa konten tersebut. keep-both = Pertahankan keduanya skip = Lewati set-executable-and-launch = Atur sebagai dijalankan dan luncurkan set-and-launch = Atur dan luncurkan set-executable-and-launch-description = Apakah anda ingin mengatur "{ $name }" sebagai dijalankan dan luncurkan? open-with = Buka dengan owner = Pemilik group = Grup other = Lainnya none = Tidak ada execute-only = Hanya jalankan write-only = Hanya tulis write-execute = Tulis dan jalankan read-only = Hanya baca read-execute = Baca dan jalankan read-write = Baca dan tulis read-write-execute = Baca, tulis, dan jalankan favorite-path-error = Galat membuka direktori remove = Hapus keep = Pertahankan repository = Repositori favorite-path-error-description = Tidak dapat membuka "{ $path }" "{ $path }" mungkin tidak ada atau anda mungkin tidak memiliki izin untuk membuka Apakah anda ingin menghapus dari bilah sisi? support = Dukungan add-network-drive = Tambahkan drive jaringan connect = Sambungkan connect-anonymously = Sambungkan secara anonim connecting = Menyambungkan... domain = Domain enter-server-address = Masukkan alamat server network-drive-description = Alamat server mencakup awalan protokol dan alamat. Contoh: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Protokol yang tersedia,Awalan AppleTalk,afp:// File Transfer Protocol,ftp:// atau ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// atau ssh:// WebDAV,dav:// atau davs:// network-drive-error = Tidak dapat mengakses drive jaringan password = Kata sandi remember-password = Ingat kata sandi try-again = Coba lagi username = Nama pengguna cancelled = Dibatalkan edit-history = Sunting riwayat history = Riwayat no-history = Tidak ada item dalam riwayat. pending = Menunggu progress = { $percent }% progress-cancelled = { $percent }%, dibatalkan progress-failed = { $percent }%, gagal progress-paused = { $percent }%, dijeda failed = Gagal complete = Selesai copy_noun = Salin creating = Membuat "{ $name }" di "{ $parent }" created = "{ $name }" dibuat di "{ $parent }" compressing = Mengompres { $items } { $items -> [one] item *[other] item } dari "{ $from }" ke "{ $to }" ({ $progress })... compressed = { $items } { $items -> [one] item *[other] item } dikompres dari "{ $from }" ke "{ $to }" copied = { $items } { $items -> [one] item *[other] item } disalin dari "{ $from }" ke "{ $to }" copying = Menyalin { $items } { $items -> [one] item *[other] item } dari "{ $from }" ke "{ $to }" ({ $progress })... deleting = Menghapus { $items } { $items -> [one] item *[other] item } dari { trash } ({ $progress })... emptied-trash = { trash } telah dikosongkan extracting = Mengekstrak { $items } { $items -> [one] item *[other] item } dari "{ $from }" ke "{ $to }" ({ $progress })... extracted = { $items } { $items -> [one] item *[other] item } diekstrak dari "{ $from }" ke "{ $to }" setting-executable-and-launching = Mengatur "{ $name }" sebagai dijalankan dan meluncurkan set-executable-and-launched = Atur "{ $name }" sebagai dijalankan dan diluncurkan setting-permissions = Mengatur izin untuk "{ $name }" ke { $mode } set-permissions = Atur izin untuk "{ $name }" ke { $mode } menu-open-with = Buka dengan... unknown-folder = map yang tidak diketahui default-app = { $name } (bawaan) show-details = Tampilkan rincian type = Jenis: { $mime } items = Item: { $items } item-size = Ukuran: { $size } moving = Memindahkan { $items } { $items -> [one] item *[other] item } dari "{ $from }" ke "{ $to }" ({ $progress })... moved = { $items } { $items -> [one] item *[other] item } dipindahkan dari "{ $from }" ke "{ $to }" permanently-deleting = Menghapus { $items } { $items -> [one] item *[other] item } secara permanen removing-from-recents = Menghapus { $items } { $items -> [one] item *[other] item } dari { recents } removed-from-recents = { $items } { $items -> [one] item *[other] item } dihapus dari { recents } renaming = Mengganti nama "{ $from }" ke "{ $to }" renamed = Nama diganti "{ $from }" ke "{ $to }" restoring = Memulihkan { $items } { $items -> [one] item *[other] item } dari { trash } ({ $progress })... restored = { $items } { $items -> [one] item *[other] item } dipulihkan dari { trash } item-created = Dibuat: { $created } item-modified = Dimodifikasi: { $modified } item-accessed = Diakses: { $accessed } calculating = Menghitung... settings = Pengaturan single-click = Klik sekali untuk membuka appearance = Tampilan theme = Tema match-desktop = Cocokkan desktop dark = Gelap light = Terang type-to-search = Ketik untuk mencari type-to-search-recursive = Mencari di map saat ini dan semua submap type-to-search-enter-path = Memasukkan jalur ke direktori atau berkas add-to-sidebar = Tambahkan ke bilah sisi compress = Kompres... eject = Keluarkan extract-here = Ekstrak new-file = Berkas baru... new-folder = Map baru... open-in-terminal = Buka di terminal move-to-trash = Pindahkan ke sampah restore-from-trash = Pulihkan dari sampah remove-from-sidebar = Hapus dari bilah sisi sort-by-name = Urutkan berdasarkan nama sort-by-modified = Urutkan berdasarkan dimodifikasi sort-by-size = Urutkan berdasarkan ukuran remove-from-recents = Hapus dari terbaru change-wallpaper = Ubah wallpaper... desktop-appearance = Tampilan desktop... display-settings = Pengaturan layar... file = Berkas new-tab = Tab baru new-window = Jendela baru reload-folder = Muat ulang map rename = Ganti nama... close-tab = Tutup tab quit = Keluar edit = Sunting cut = Potong copy = Salin paste = Tempel select-all = Pilih semua zoom-in = Perbesar default-size = Ukuran bawaan zoom-out = Perkecil view = Tampilan grid-view = Tampilan kisi list-view = Tampilan daftar gallery-preview = Tampilan galeri show-hidden-files = Tampilkan berkas tersembunyi list-directories-first = Daftar direktori terlebih dahulu menu-settings = Pengaturan... menu-about = Tentang Berkas COSMIC... sort = Urutkan sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Terbaru terlebih dahulu sort-oldest-first = Tertua terlebih dahulu sort-smallest-to-largest = Terkecil hingga terbesar sort-largest-to-smallest = Terbesar hingga terkecil selected-items = { $items } item yang dipilih type-to-search-select = Memilih berkas atau map pertama yang cocok pasted-image = Gambar Ditempel pasted-text = Teks Ditempel pasted-video = Video Ditempel move-to = Pindahkan ke... copy-to = Salin ke... move-to-button-label = Pindahkan move-to-title = Pilih destinasi pindahan copy-to-button-label = Salin copy-to-title = Pilih destinasi salinan keywords = Map;Pengelola; 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 ================================================ FILE: i18n/ie/cosmic_files.ftl ================================================ ================================================ FILE: i18n/is/cosmic_files.ftl ================================================ cancel = Hætta við support = Stuðningur delete = Eyða name = Heiti settings = Stillingar appearance = Útlit theme = Þema match-desktop = Passa við skjáborð dark = Dökkt light = Ljóst file = Skrá new-tab = Nýr flipi new-window = Nýr gluggi close-tab = Loka flipa quit = Hætta edit = Breyta copy = Afrita paste = Líma select-all = Velja allt password = Lykilorð skip = Sleppa cosmic-files = COSMIC Skráastjóri empty-folder = Tóm mappa empty-folder-hidden = Tóm mappa (inniheldur falin atriði) no-results = Engar niðurstöður fundust filesystem = Skráakerfi home = Heimamappa networks = Net notification-in-progress = Skráaaðgerðir er í gangi. trash = Rusl recents = Nýlegt undo = Afturkalla today = Í dag desktop-view-options = Valkostir skjáborðsyfirlits... show-on-desktop = Sýna á skjáborði desktop-folder-content = Innihald skjáborðsmöppu mounted-drives = Tengd drif trash-folder-icon = Tákn ruslamöppu icon-size-and-spacing = Stærð og bil tákna icon-size = Táknastærð grid-spacing = Bil í reitayfirliti modified = Breytt trashed-on = Sett í rusl size = Stærð details = Upplýsingar dismiss = Hunsa skilaboð operations-running = { $running } { $running -> [one] aðgerð *[other] aðgerðir } í gangi ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] aðgerð *[other] aðgerðir } í gangi ({ $percent }%), { $finished } lokið... pause = Á pásu resume = Halda áfram create-archive = Búa til safnskrá extract-password-required = Lykilorðs krafist extract-to = Afþjappa í... extract-to-title = Afþjappa í möppu empty-trash = Tæma rusl empty-trash-warning = Viltu örugglega eyða endanlega öllum atriðunum í Ruslmöppunni? mount-error = Get ekki opnað drif create-new-file = Búa til nýja skrá create-new-folder = Búa til nýja möppu file-name = Skrárheiti folder-name = Möppuheiti file-already-exists = Skrá með þessu heiti er til nú þegar. folder-already-exists = Mappa með þessu heiti er til nú þegar. name-hidden = Heiti sem byrja á „.“ verða falin. name-invalid = Heiti má ekki vera „{ $filename }“. name-no-slashes = Heiti má ekki innihalda skrástrik. create = Búa til open = Opna open-file = Opna skrá open-folder = Opna möppu open-in-new-tab = Opna í nýjum flipa open-in-new-window = Opna í nýjum glugga open-item-location = Opna staðsetningu atriðis open-multiple-files = Opna margar skrár open-multiple-folders = Opna margar möppur save = Vista save-file = Vista skrá open-with-title = Hvernig viltu opna „{ $name }“? browse-store = Skoða { $store } other-apps = Önnur forrit related-apps = Tengd forrit selected-items = völdu atriðin { $items } permanently-delete-question = Eyða varanlega permanently-delete-warning = Viltu örugglega eyða { $target } varanlega? Það er ekki hægt að afturkalla. rename-file = Endurnefna skrá rename-folder = Endurnefna möppu replace = Skipta út replace-title = „{ $filename }“ er nú þegar til á þessum stað. replace-warning = Viltu skipta henni út fyrir þá sem þú ert að vista? Ef henni er skipt út verður skrifað yfir innihald hennar. replace-warning-operation = Viltu skipta henni út? Ef henni er skipt út verður skrifað yfir innihald hennar. original-file = Upprunaleg skrá replace-with = Skipta út fyrir apply-to-all = Nota á allt keep-both = Halda báðum set-executable-and-launch = Gera að keyrsluskrá og keyra set-executable-and-launch-description = Viltu gera „{ $name }“ að keyrsluskrá og keyra hana? set-and-launch = Stilla og keyra open-with = Opna með owner = Eigandi group = Hópur other = Annað none = Ekkert read-only = Skrifvarin read-write = Lesa og skrifa read-write-execute = Lesa, skrifa og keyra favorite-path-error = Villa við að opna möppu favorite-path-error-description = Gat ekki opnað „{ $path }“. Kannski er hún ekki til eða þú hefur ekki heimild til að opna hana. Viltu fjarlægja hana úr hliðarstikunni? remove = Fjarlægja keep = Geyma add-network-drive = Bæta við netdrifi connect = Tengjast connect-anonymously = Tengjast nafnlaust connecting = Tengist… domain = Lén enter-server-address = Sláðu inn vistfang þjóns network-drive-description = Vistföng þjóns innihalda forskeyti og vistfang. Dæmi: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Tiltækir samskiptastaðlar,forskeyti AppleTalk,afp:// File Transfer Protocol,ftp:// eða ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// eða ssh:// WebDAV,dav:// eða davs:// network-drive-error = Gat ekki opnað netdrif remember-password = Muna lykilorð try-again = Reyna aftur username = Notandanafn cancelled = Hætt við history = Ferill no-history = Engin atriði í ferilskrá. progress-cancelled = { $percent }%, hætt við progress-failed = { $percent }%, mistókst progress-paused = { $percent }%, á pásu failed = Mistókst complete = Lokið compressing = Þjappa { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ ({ $progress })... compressed = Þjappaði { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ copy_noun = Afrita creating = Bý til „{ $name }“ í „{ $parent }“ created = Bjó til „{ $name }“ í „{ $parent }“ execute-only = Keyra eingöngu write-only = Aðeins skrifréttindi write-execute = Skrif- og keyrsluréttindi read-execute = Les- og keyrsluréttindi repository = Hugbúnaðarsafn edit-history = Breytingaferill pending = Í bið copying = Afrita { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ ({ $progress })... copied = Afritaði { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ deleting = Eyði { $items } { $items -> [one] atriði *[other] atriðum } úr { trash } ({ $progress })... deleted = Eyddi { $items } { $items -> [one] atriði *[other] atriðum } úr { trash } emptying-trash = Tæmi { trash } ({ $progress })... emptied-trash = Tæmdi { trash } extracting = Afþjappa { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ ({ $progress })... extracted = Afþjappaði { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ setting-executable-and-launching = Stillingin „{ $name }“ er keyranleg og að ræsast set-executable-and-launched = Stilla „{ $name }“ sem keyranlega og ræst setting-permissions = Stilli heimildir fyrir „{ $name }“ á { $mode } unknown-folder = óþekkt mappa menu-open-with = Opna með... default-app = { $name } (sjálfgefið) show-details = Sýna upplýsingar type = Gerð: { $mime } items = Atriði: { $items } item-size = Stærð: { $size } item-created = Búið til: { $created } item-modified = Breytt: { $modified } item-accessed = Opnað: { $accessed } calculating = Reikna... single-click = Smella einu sinni til að opna type-to-search = Slá inn til að leita type-to-search-recursive = Leitar í núverandi möppu og öllum undirmöppum type-to-search-enter-path = Setur inn slóðina að möppunni eða skránni add-to-sidebar = Bæta við hliðarstiku compress = Þjappa delete-permanently = Eyða varanlega eject = Ýta út extract-here = Afþjappa new-file = Ný skrá... new-folder = Ný mappa... open-in-terminal = Opna í skjáhermi move-to-trash = Færa í ruslið restore-from-trash = Endurheimta úr ruslinu remove-from-sidebar = Fjarlægja af hliðarstiku sort-by-name = Raða eftir heiti sort-by-modified = Raða eftir breytingadegi sort-by-size = Raða eftir stærð sort-by-trashed = Raða eftir eyðingartíma remove-from-recents = Fjarlægja úr nýlegu change-wallpaper = Skipta um veggfóður... desktop-appearance = Útlit skjáborðs... display-settings = Skjástillingar... reload-folder = Endurhlaða möppu rename = Endurnefna... cut = Klippa zoom-in = Auka aðdrátt default-size = Sjálfgefin stærð zoom-out = Minnka aðdrátt view = Skoða grid-view = Reitayfirlit list-view = Listayfirlit show-hidden-files = Sýna faldar skrár list-directories-first = Hafa möppur fremst gallery-preview = Forskoðun myndasafns menu-settings = Stillingar... menu-about = Um COSMIC skráastjórann... sort = Raða sort-a-z = A-Ö sort-z-a = Ö-A sort-newest-first = Nýjast fyrst sort-oldest-first = Elst fyrst sort-smallest-to-largest = Minnsta til stærsta sort-largest-to-smallest = Stærsta til minnsta progress = { $percent }% set-permissions = Stilla heimildir fyrir „{ $name }“ á { $mode } moving = Færi { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ ({ $progress })... moved = Færði { $items } { $items -> [one] atriði *[other] atriði } úr „{ $from }“ í „{ $to }“ permanently-deleting = Eyði varanlega { $items } { $items -> [one] atriði *[other] atriðum } permanently-deleted = Eyddi varanlega { $items } { $items -> [one] atriði *[other] atriðum } removing-from-recents = Fjarlægi { $items } { $items -> [one] atriði *[other] atriði } úr { recents } removed-from-recents = Fjarlægði { $items } { $items -> [one] atriði *[other] atriði } úr { recents } renaming = Endurnefni „{ $from }“ í „{ $to }“ renamed = Endurnefndi „{ $from }“ í „{ $to }“ restoring = Endurheimti { $items } { $items -> [one] atriði *[other] atriði } úr { trash } ({ $progress })... restored = Endurheimti { $items } { $items -> [one] atriði *[other] atriði } úr { trash } ================================================ FILE: i18n/it/cosmic_files.ftl ================================================ cosmic-files = COSMIC Files comment = File manager di COSMIC keywords = File;Archivi;Cartelle;Explorer; empty-folder = Cartella vuota empty-folder-hidden = Cartella vuota (con elementi nascosti) no-results = Nessun risultato trovato filesystem = Filesystem home = Home networks = Reti notification-in-progress = Operazioni sui file in corso. trash = Cestino recents = Recenti undo = Annulla today = Oggi # Desktop view options desktop-view-options = Impostazioni visualizzazione Desktop... show-on-desktop = Mostra sul Desktop desktop-folder-content = Contenuto cartella del Desktop mounted-drives = Dispositivi montati trash-folder-icon = Icona del cestino icon-size-and-spacing = Dimensioni e spaziatura icona icon-size = Dimensione icona grid-spacing = Spaziatura griglia # List view name = Nome modified = Modificato trashed-on = Cestinato size = Dimensione # Progress footer details = Dettagli dismiss = Nascondi messaggio operations-running = { $running } { $running -> [one] operazione *[other] operazioni } in corso ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operazione *[other] operazioni } in corso ({ $percent }%), { $finished } completata... pause = Pausa resume = Riprendi # Dialogs ## Compress Dialog create-archive = Crea archivio ## Extract Dialog extract-password-required = Password richiesta extract-to = Estrai in... extract-to-title = Estrai nella cartella ## Empty Trash Dialog empty-trash = Svuota cestino empty-trash-warning = Sei sicuro di voler eliminare definitivamente tutti gli elementi nel cestino? ## Mount Error Dialog mount-error = Impossibile accedere al dispositivo ## New File/Folder Dialog create-new-file = Crea un nuovo file create-new-folder = Crea una nuova cartella file-name = Nome file folder-name = Nome cartella file-already-exists = Esiste già un file con questo nome. folder-already-exists = Esiste già una cartella con questo nome. name-hidden = I nomi che iniziano con "." verranno nascosti. name-invalid = Il nome non può essere "{ $filename }". name-no-slashes = I nomi non possono contenere gli slash. ## Open/Save Dialog cancel = Annulla create = Crea open = Apri open-file = Apri file open-folder = Apri cartella open-in-new-tab = Apri in una nuova scheda open-in-new-window = Apri in una nuova finestra open-item-location = Apri percorso file open-multiple-files = Apri files multipli open-multiple-folders = Apri cartelle multiple save = Salva save-file = Salva file ## Open With Dialog open-with-title = Come vuoi aprire il file "{ $name }"? browse-store = Cerca in { $store } other-apps = Altre applicazioni related-apps = Applicazioni simili ## Permanently delete Dialog selected-items = i { $items } elementi selezionati permanently-delete-question = Elimina definitivamente delete = Elimina permanently-delete-warning = Sei sicuro di voler eliminare definitivamente { $target }? Questa azione non può essere annullata. ## Rename Dialog rename-file = Rinomina file rename-folder = Rinomina cartella ## Replace Dialog replace = Sostituisci replace-title = "{ $filename }" esiste già in questo percorso. replace-warning = Vuoi sostituirlo con quello che stai per salvare? La sostituzione sovrascriverà il suo contenuto. replace-warning-operation = Vuoi sostituirlo? La sostituzione sovrascriverà il suo contenuto. original-file = File originale replace-with = Sostituisci con apply-to-all = Applica a tutti keep-both = Mantieni entrambi skip = Salta ## Set as Executable and Launch Dialog set-executable-and-launch = Imposta come "eseguibile" e apri set-executable-and-launch-description = Vuoi impostare "{ $name }" come "eseguibile" e aprirlo? set-and-launch = Imposta e apri ## Metadata Dialog open-with = Apri con owner = Proprietario group = Gruppo other = Altro ### Mode 0 none = Nessuno ### Mode 1 (unusual) execute-only = Sola esecuzione ### Mode 2 (unusual) write-only = Sola scrittura ### Mode 3 (unusual) write-execute = Scrittura ed esecuzione ### Mode 4 read-only = Sola lettura ### Mode 5 read-execute = Lettura e esecuzione ### Mode 6 read-write = Lettura e scrittura ### Mode 7 read-write-execute = Lettura, scrittura ed esecuzione ## Favorite Path Error Dialog favorite-path-error = Errore nell'apertura della cartella favorite-path-error-description = Impossibile aprire "{ $path }". Potrebbe non esistere o potresti non avere i permessi di accesso. Vuoi rimuoverla dalla barra laterale? remove = Rimuovi keep = Mantieni # Context Pages ## About repository = Repository support = Supporto ## Add Network Drive add-network-drive = Aggiungi dispositivo di rete connect = Connetti connect-anonymously = Connetti in modo anonimo connecting = Connessione in corso... domain = Dominio enter-server-address = Inserisci indirizzo di rete network-drive-description = Gli indirizzi dei server includono il prefisso del protocollo e l'indirizzo. Esempi: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocolli disponibili,Prefisso AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDav,dav:// or davs:// network-drive-error = Impossibile accedere al dispositivo di rete password = Password remember-password = Ricorda password try-again = Riprova username = Nome utente ## Operations cancelled = Annullato edit-history = Modifica cronologia history = Cronologia no-history = Nessun elemento nella cronologia. pending = In coda progress = { $percent }% progress-cancelled = { $percent }%, annullato progress-paused = { $percent }%, in pausa failed = Fallito complete = Completato compressing = Compressione in corso di { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" ({ $progress })... compressed = Compressi { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" copy_noun = Copia creating = Creazione "{ $name }" in "{ $parent }" created = Creato "{ $name }" in "{ $parent }" copying = Copia in corso di { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" ({ $progress })... copied = Copiati { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" deleting = Eliminazione in corso di { $items } { $items -> [one] elemento *[other] elementi } dal { trash }: ({ $progress })... deleted = Eliminati { $items } { $items -> [one] elemento *[other] elementi } dal { trash } emptying-trash = Svuotamento del { trash }: ({ $progress })... emptied-trash = { trash } svuotato extracting = Estrazione in corso di { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }": ({ $progress })... extracted = Estratti { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" setting-executable-and-launching = Impostazione in corso di "{ $name }" come "eseguibile" e avvio set-executable-and-launched = Impostato "{ $name }" come "eseguibile" e avviato moving = Spostamento in corso di { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }": ({ $progress })... moved = Spostati { $items } { $items -> [one] elemento *[other] elementi } da "{ $from }" a "{ $to }" renaming = Rinominazione di "{ $from }" in "{ $to }" renamed = Rinominato "{ $from }" in "{ $to }" restoring = Ripristino in corso di { $items } { $items -> [one] elemento *[other] elementi } dal { trash }: ({ $progress })... restored = Ripristinati { $items } { $items -> [one] elemento *[other] elementi } dal { trash } unknown-folder = cartella sconosciuta ## Open with menu-open-with = Apri con... default-app = { $name } (predefinito) ## Show details show-details = Mostra dettagli type = Tipo: { $mime } items = Files: { $items } item-size = Dimensione: { $size } item-created = Creato: { $created } item-modified = Modificato in data: { $modified } item-accessed = Accesso eseguito in data: { $accessed } calculating = Calcolo in corso... ## Settings settings = Impostazioni single-click = Click singolo per aprire ### Appearance appearance = Aspetto theme = Tema match-desktop = Sistema dark = Scuro light = Chiaro ### Type to Search type-to-search = Digita per cercare type-to-search-recursive = Cerca nella cartella attuale e nelle sue sotto-cartelle type-to-search-enter-path = Inserisci il percorso della cartella o del file # Context menu add-to-sidebar = Aggiungi alla barra laterale compress = Comprimi delete-permanently = Eliminazione definitiva eject = Espelli extract-here = Estrai new-file = Nuovo file... new-folder = Nuova cartella... open-in-terminal = Apri nel terminale move-to-trash = Sposta nel cestino restore-from-trash = Ripristina dal cestino remove-from-sidebar = Rimuovi dalla barra laterale sort-by-name = Ordina per nome sort-by-modified = Ordina per data di modifica sort-by-size = Ordina per dimensione sort-by-trashed = Ordina per data di eliminazione remove-from-recents = Rimuovi da recenti ## Desktop change-wallpaper = Modifica sfondo... desktop-appearance = Aspetto del Desktop... display-settings = Impostazioni del display... # Menu ## File file = File new-tab = Nuova scheda new-window = Nuova finestra reload-folder = Aggiorna cartella rename = Rinomina... close-tab = Chiudi scheda quit = Esci ## Edit edit = Modifica cut = Taglia copy = Copia paste = Incolla select-all = Seleziona tutto ## View zoom-in = Aumenta zoom default-size = Dimensione predefinita zoom-out = Diminuisci zoom view = Visualizza grid-view = Visualizzazione a griglia list-view = Visualizzazione a elenco show-hidden-files = Mostra file nascosti list-directories-first = Mostra prima le cartelle gallery-preview = Anteprima immagine menu-settings = Impostazioni... menu-about = Informazioni su COSMIC Files... ## Sort sort = Ordina sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Prima i più recenti sort-oldest-first = Prima i più vecchi sort-smallest-to-largest = Dal più piccolo al più grande sort-largest-to-smallest = Dal più grande al più piccolo progress-failed = { $percent }%, fallito setting-permissions = Impostazione dei permessi per "{ $name }" su { $mode } set-permissions = Permessi impostati per "{ $name }" su { $mode } permanently-deleting = Eliminazione definitva in corso di { $items } { $items -> [one] elemento *[other] elementi } permanently-deleted = Eliminazione definitivamente { $items } { $items -> [one] elemento *[other] elementi } removing-from-recents = Rimozione in corso di { $items } { $items -> [one] elemento *[other] elementi } da { recents } removed-from-recents = Rimossi { $items } { $items -> [one] elemento *[other] elementi } da { recents } ================================================ FILE: i18n/ja/cosmic_files.ftl ================================================ cosmic-files = COSMICファイル empty-folder = 空のフォルダ empty-folder-hidden = 空のフォルダ(隠しファイルあり) no-results = 検索結果はありません filesystem = ファイルシステム home = ホーム networks = ネットワーク notification-in-progress = ファイル処理が進行中です。 trash = ゴミ箱 recents = 最近 undo = 元に戻す # List view name = 名前 modified = 更新日 size = サイズ # Dialogs ## Compress Dialog create-archive = アーカイブを作成 ## Empty Trash Dialog empty-trash = ゴミ箱を空にする empty-trash-warning = ゴミ箱のアイテムをすべて完全に削除してもよろしいですか? # New File/Folder Dialog create-new-file = 新しいファイルを作成 create-new-folder = 新しいフォルダを作成 file-name = ファイル名 folder-name = フォルダ名 file-already-exists = 同じ名前のファイルがすでに存在します。 folder-already-exists = 同じ名前のフォルダがすでに存在します。 name-hidden = 「.」で始まる名前は隠られます。 name-invalid = 「{ $filename }」という名前は使用できません。 name-no-slashes = 「/」は名前に含められません。 # Open/Save Dialog cancel = キャンセル create = 作る open = 開く open-file = ファイルを開く open-folder = フォルダを開く open-in-new-tab = 新しいタブで開く open-in-new-window = 新しいウィンドウで開く open-item-location = アイテムの場所を開く open-multiple-files = 複数ファイルを開く open-multiple-folders = 複数フォルダを開く save = 保存 save-file = ファイルを保存 # Rename Dialog rename-file = ファイル名を変更 rename-folder = フォルダ名を変更 # Replace Dialog replace = 置き換える replace-title = { $filename }はすでにこの場所に存在します。 replace-warning = 保存しているファイルで置き換えますか?置き換えると、内容を上書きます。 replace-warning-operation = 置き換えますか?置き換えると、内容を上書きます。 original-file = 元のファイル replace-with = これで置き換える: apply-to-all = 全てに適用 keep-both = 両方を保管 skip = スキップ ## Metadata Dialog owner = 所有者 group = グループ other = その他 # Context Pages ## About ## Add Network Drive add-network-drive = ネットワークドライブを追加 connect = 接続する connect-anonymously = 匿名的に接続 connecting = 接続中... domain = ドメイン enter-server-address = サーバーアドレスを入力 network-drive-description = サーバーアドレスはプロトコル接頭辞とアドレスを含めます。 例: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns 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:// network-drive-error = ネットワークドライブにアクセスできませんでした password = パスワード remember-password = パスワードを覚える try-again = 再試行 username = ユーザー名 ## Operations edit-history = 履歴を編集 history = 履歴 no-history = 履歴はありません。 pending = 保留中 failed = 失敗 complete = 完了 compressing = "{ $from }" から "{ $to }" へ { $items }個のアイテムを圧縮中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… compressed = "{ $from }" から "{ $to }" へ { $items }個のアイテムを圧縮しました { $items -> [one] 項目 *[other] 項目 } copy_noun = コピー creating = { $parent }で{ $name }を作成中 created = { $parent }で{ $name }を作成完了 copying = "{ $from }" から "{ $to }" へ { $items }個のアイテムをコピー中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… copied = "{ $from }" から "{ $to }" へ { $items }個のアイテムをコピーしました { $items -> [one] 項目 *[other] 項目 } emptying-trash = { trash }を空にしています ({ $progress })… emptied-trash = { trash }を空にした moving = "{ $from }" から "{ $to }" へ { $items }個のアイテムを移動中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… moved = "{ $from }" から "{ $to }" へ { $items }個のアイテムを移動しました { $items -> [one] 項目 *[other] 項目 } renaming = { $from }を{ $to }に変更中 renamed = { $from }を{ $to }に変更完了 restoring = { trash }から{ $items }個のアイテムを復元中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… restored = { trash }から{ $items }個のアイテムを復元しました { $items -> [one] 項目 *[other] 項目 } unknown-folder = 不明なフォルダー ## Open with menu-open-with = 別のアプリケーションで開く... default-app = { $name } (デフォルト) ## Properties ## Settings settings = 設定 ### Appearance appearance = 外観 theme = テーマ match-desktop = システム設定に従う dark = ダーク light = ライト # Context menu add-to-sidebar = サイドバーに追加 compress = 圧縮 extract-here = 抽出 new-file = 新しいファイル... new-folder = 新しいフォルダ... open-in-terminal = 端末で開く move-to-trash = ゴミ箱に移動 restore-from-trash = ゴミ箱から復元 remove-from-sidebar = サイドバーから削除 sort-by-name = 名前で並べ替え sort-by-modified = 更新日で並べ替え sort-by-size = サイズで並べ替え # Menu ## File file = ファイル new-tab = 新しいタブ new-window = 新しいウィンドウ rename = 名前を変更... close-tab = タブを閉じる quit = 終了 ## Edit edit = 編集 cut = 切り取り copy = コピー paste = 貼り付け select-all = すべてを選択 ## View zoom-in = ズームイン default-size = 規定のサイズ zoom-out = ズームアウト view = 表示 grid-view = グリッドの表示 list-view = リストの表示 show-hidden-files = 隠しファイルを表示 list-directories-first = フォルダを最初に表示 menu-settings = 設定... 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 = 最大から最小まで repository = リポジトリ support = サポート remove = 削除 today = 今日 desktop-view-options = デスクトップの表示オプション… show-on-desktop = デスクトップの表示オプション desktop-folder-content = デスクトップフォルダの内容 mounted-drives = マウント済みドライブ trash-folder-icon = ゴミ箱のアイコン icon-size-and-spacing = アイコンのサイズと間隔 icon-size = アイコンサイズ grid-spacing = グリッドの間隔 trashed-on = ゴミ箱に入れた日時 details = 詳細 dismiss = メッセージを閉じる operations-running = { $running } { $running -> [one] 件の操作が *[other] 件の操作が }実行中です({ $percent }%)... operations-running-finished = { $running } { $running -> [one] 件の操作が *[other] 件の操作が } 実行中です({ $percent }%)、 { $finished } 件が終了... pause = 一時停止 resume = 一時停止 extract-password-required = パスワードが必要です extract-to = 展開先… extract-to-title = フォルダーに展開 mount-error = ドライブにアクセスできません open-with-title = 「{ $name }」をどのように開きますか? browse-store = { $store } を参照 other-apps = 他のアプリケーション related-apps = 関連アプリケーション selected-items = 選択された{ $items }個のアイテム permanently-delete-question = 完全に削除 delete = 削除 permanently-delete-warning = { $target }を完全に削除してもよろしいですか?この操作は元に戻せません。 set-executable-and-launch = 実行可能にして起動 set-executable-and-launch-description = "{ $name }"を実行可能に設定して起動しますか? set-and-launch = 設定して起動 open-with = 別のアプリケーションで開く none = なし execute-only = 実行のみ write-only = 書き込み専用 write-execute = 書き込みと実行 read-only = 読み取り専用 read-execute = 読み取りと実行 read-write = 読み取りと書き込み read-write-execute = 読み取り、書き込み、実行 favorite-path-error = ディレクトリを開けませんでした favorite-path-error-description = "{ $path }"を開けません。 このパスが存在しないか、開くための権限がない可能性があります。 サイドバーから削除しますか? keep = そのままにする cancelled = キャンセルされました progress = { $percent } % progress-cancelled = { $percent } %、キャンセルされました progress-failed = { $percent } %、失敗 progress-paused = { $percent } %、一時停止中 deleting = { $items }個のアイテムを{ trash }から削除中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… deleted = { $items } 個のアイテムを { trash } から削除しました { $items -> [one] 項目 *[other] 項目 } extracting = "{ $from }" から "{ $to }" へ { $items } 個のアイテムを展開中 ({ $progress }) { $items -> [one] 項目 *[other] 項目 }… extracted = "{ $from }" から "{ $to }" へ { $items } 個のアイテムを展開しました { $items -> [one] 項目 *[other] 項目 } setting-executable-and-launching = "{ $name }"を実行可能に設定して起動中 set-executable-and-launched = "{ $name }"を実行可能に設定して起動しました setting-permissions = "{ $name }"のパーミッションを{ $mode }に設定中 set-permissions = "{ $name }"のパーミッションを{ $mode }に設定しました permanently-deleting = { $items }個のアイテムを完全に削除中 { $items -> [one] 項目 *[other] 項目 } permanently-deleted = { $items }個のアイテムを完全に削除しました { $items -> [one] 項目 *[other] 項目 } removing-from-recents = { $items }個のアイテムを{ recents }から削除中 { $items -> [one] 項目 *[other] 項目 } removed-from-recents = { $items }個のアイテムを{ recents }から削除しました { $items -> [one] 項目 *[other] 項目 } show-details = 詳細を表示 type = 種類: { $mime } items = アイテム: { $items } item-size = サイズ: { $size } item-created = 作成日時: { $created } item-modified = 最終更新日時: { $modified } item-accessed = 最終アクセス日時: { $accessed } calculating = 計算中… single-click = シングルクリックで開く type-to-search = 入力して検索 type-to-search-recursive = 現在のフォルダーとすべてのサブフォルダーを検索 type-to-search-enter-path = ディレクトリーまたはファイルのパスを入力 delete-permanently = 完全に削除する eject = 取り出し sort-by-trashed = 削除日時 remove-from-recents = 最近の項目から削除 change-wallpaper = 壁紙を変更… desktop-appearance = デスクトップの見た目… display-settings = ディスプレイの設定… reload-folder = フォルダーを再読み込み gallery-preview = ギャラリープレビュー ================================================ FILE: i18n/jv/cosmic_files.ftl ================================================ ================================================ FILE: i18n/ka/cosmic_files.ftl ================================================ ================================================ FILE: i18n/kab/cosmic_files.ftl ================================================ 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 ================================================ FILE: i18n/kk/cosmic_files.ftl ================================================ cosmic-files = COSMIC файлдары empty-folder = Бос бума empty-folder-hidden = Бос бума (жасырын элементтері бар) no-results = Нәтижелер табылмады filesystem = Файлдық жүйе home = Үй networks = Желілер notification-in-progress = Файлдармен әрекеттер орындалуда trash = Қоқыс шелегі recents = Соңғылар undo = Болдырмау today = Бүгін desktop-view-options = Жұмыс үстелінің көрініс опциялары... show-on-desktop = Жұмыс үстелінде көрсету desktop-folder-content = Жұмыс үстелі бумасының мазмұны mounted-drives = Тіркелген дискілер trash-folder-icon = Қоқыс шелегі бумасының таңбашасы icon-size-and-spacing = Таңбаша өлшемі мен аралықтары icon-size = Таңбаша өлшемі grid-spacing = Тор аралықтары name = Аты modified = Өзгертілген trashed-on = Қоқыс шелегіне тасталған size = Өлшемі details = Ақпараты dismiss = Хабарламаны елемеу operations-running = { $running } { $running -> [one] әрекет *[other] әрекет } орындалуда ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] әрекет *[other] әрекет } орындалуда ({ $percent }%), { $finished } аяқталды... pause = Аялдату resume = Жалғастыру create-archive = Архив жасау extract-password-required = Пароль керек extract-to = Шығару... extract-to-title = Бумаға шығару empty-trash = Себетті тазарту empty-trash-title = Себетті тазарту керек пе? empty-trash-warning = Себет бумасындағы элементтер біржола өшіріледі mount-error = Дискіге қол жеткізу мүмкін емес create-new-file = Жаңа файл жасау create-new-folder = Жаңа бума жасау file-name = Файл аты folder-name = Бума аты file-already-exists = Ондай аты бар файл бұрыннан бар folder-already-exists = Ондай аты бар бума бұрыннан бар name-hidden = "." таңбасынан басталатын атаулар жасырын болады name-invalid = Аты "{ $filename }" болуы мүмкін емес name-no-slashes = Атауда қиғаш сызықтар болмауы тиіс cancel = Бас тарту create = Жасау open = Ашу open-file = Файлды ашу open-folder = Буманы ашу open-in-new-tab = Жаңа бетте ашу open-in-new-window = Жаңа терезеде ашу open-item-location = Нысанның орнын ашу open-multiple-files = Бірнеше файлды ашу open-multiple-folders = Бірнеше буманы ашу save = Сақтау save-file = Файлды сақтау open-with-title = "{ $name }" қалай ашқыңыз келеді? browse-store = { $store } шолу other-apps = Басқа қолданбалар related-apps = Қатысты қолданбалар selected-items = Таңдалған { $items } нысан permanently-delete-question = Біржола өшіру керек пе? delete = Өшіру permanently-delete-warning = { $target } біржола өшіріледі. Бұл әрекетті болдырмау мүмкін емес. rename-file = Файлдың атын өзгерту rename-folder = Буманың атын өзгерту replace = Алмастыру replace-title = Бұл жерде "{ $filename }" бұрыннан бар replace-warning = Оны сақталып жатқан файлмен алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны қайта жазылады. replace-warning-operation = Оны алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны үстінен жазылады. original-file = Түпнұсқа файл replace-with = Келесімен алмастыру apply-to-all = Барлығына іске асыру keep-both = Екеуін де қалдыру skip = Өткізіп жіберу set-executable-and-launch = Орындалатын файл ретінде орнату және жөнелту set-executable-and-launch-description = "{ $name }" нысанын орындалатын файл ретінде орнатып, оны жөнелтуді қалайсыз ба? set-and-launch = Орнату және жөнелту open-with = Көмегімен ашу owner = Иесі group = Топ other = Басқа none = Ештеңе execute-only = Тек орындау write-only = Тек жазу write-execute = Жазу және орындау read-only = Тек оқу read-execute = Оқу және орындау read-write = Оқу және жазу read-write-execute = Оқу, жазу және орындау favorite-path-error = Буманы ашу қатесі favorite-path-error-description = "{ $path }" ашу мүмкін емес "{ $path }" жоқ болуы мүмкін немесе оны ашуға құқығыңыз жоқ Оны бүйірлік панельден өшіруді қалайсыз ба? remove = Өшіру keep = Қалдыру repository = Репозиторий support = Қолдау add-network-drive = Желілік дискіні қосу connect = Байланысу connect-anonymously = Анонимді түрде байланысу connecting = Байланысуда... domain = Домен enter-server-address = Сервер адресін енгізіңіз 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:// network-drive-error = Желілік дискіге қол жеткізу мүмкін емес password = Пароль remember-password = Парольді есте сақтау try-again = Қайтадан көру username = Пайдаланушы аты cancelled = Бас тартылды edit-history = Тарихты түзету history = Тарихы no-history = Тарихта ешқандай элемент жоқ. pending = Күтілуде progress = { $percent } % progress-cancelled = { $percent }%, бас тартылды progress-failed = { $percent }%, сәтсіз аяқталды progress-paused = { $percent }%, аялдатылды failed = Сәтсіз аяқталды complete = Аяқталды compressing = { $items } { $items -> [one] нәрсені *[other] нәрсені } "{ $from }" ішінен "{ $to }" ішіне сығу ({ $progress })... compressed = { $items } { $items -> [one] нәрсе *[other] нәрсе } "{ $from }" ішінен "{ $to }" ішіне сығылды copy_noun = Көшіріп алу creating = "{ $parent }" ішінде "{ $name }" жасау created = "{ $parent }" ішінде "{ $name }" жасалды copying = { $items } { $items -> [one] нәрсені *[other] нәрсені } "{ $from }" ішінен "{ $to }" ішіне көшіру ({ $progress })... copied = { $items } { $items -> [one] нәрсе *[other] нәрсе } "{ $from }" ішінен "{ $to }" ішіне көшірілді deleting = { $items } { $items -> [one] нәрсені *[other] нәрсені } { trash } ішінен өшіру ({ $progress })... deleted = { $items } { $items -> [one] нәрсе *[other] нәрсе } { trash } ішінен өшірілді emptying-trash = { trash } тазартылуда ({ $progress })... emptied-trash = { trash } тазартылды extracting = { $items } { $items -> [one] нәрсені *[other] нәрсені } "{ $from }" ішінен "{ $to }" ішіне тарқату ({ $progress })... extracted = { $items } { $items -> [one] нәрсе *[other] нәрсе } "{ $from }" ішінен "{ $to }" ішіне тарқатылды setting-executable-and-launching = "{ $name }" орындалатын файл ретінде орнату және іске қосу set-executable-and-launched = "{ $name }" орындалатын файл ретінде орнатылды және іске қосылды setting-permissions = "{ $name }" үшін рұқсаттарды { $mode } мәніне орнату set-permissions = "{ $name }" үшін рұқсаттар { $mode } мәніне орнатылды moving = { $items } { $items -> [one] нәрсені *[other] нәрсені } "{ $from }" ішінен "{ $to }" ішіне жылжыту ({ $progress })... moved = { $items } { $items -> [one] нәрсе *[other] нәрсе } "{ $from }" ішінен "{ $to }" ішіне жылжытылды permanently-deleting = { $items } { $items -> [one] нәрсені *[other] нәрсені } біржола өшіру permanently-deleted = { $items } { $items -> [one] нәрсе *[other] нәрсе } біржола өшірілді removing-from-recents = { $items } { $items -> [one] нәрсені *[other] нәрсені } { recents } тізімінен өшіру removed-from-recents = { $items } { $items -> [one] нәрсе *[other] нәрсе } { recents } тізімінен өшірілді renaming = "{ $from }" атын "{ $to }" деп өзгерту renamed = "{ $from }" аты "{ $to }" деп өзгертілді restoring = { $items } { $items -> [one] нәрсені *[other] нәрсені } { trash } ішінен қалпына келтіру ({ $progress })... restored = { $items } { $items -> [one] нәрсе *[other] нәрсе } { trash } ішінен қалпына келтірілді unknown-folder = белгісіз бума menu-open-with = Көмегімен ашу... default-app = { $name } (әдепкі) show-details = Мәліметтерді көрсету type = Түрі: { $mime } items = Элементтер: { $items } item-size = Өлшемі: { $size } item-created = Жасалған: { $created } item-modified = Өзгертілген: { $modified } item-accessed = Қол жеткізілген: { $accessed } calculating = Есептеу... settings = Баптаулар single-click = Ашу үшін бір рет шерту appearance = Сыртқы түрі theme = Тақырып match-desktop = Жұмыс үстеліне сәйкес келу dark = Күңгірт light = Ашық type-to-search = Іздеу үшін теру type-to-search-recursive = Ағымдағы бума мен барлық ішкі бумаларды іздейді type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды add-to-sidebar = Бүйірлік панельге қосу compress = Сығу... delete-permanently = Біржолата өшіру eject = Шығару extract-here = Тарқату new-file = Жаңа файл... new-folder = Жаңа бума... open-in-terminal = Терминалда ашу move-to-trash = Қоқыс жәшігіне тастау restore-from-trash = Қоқыс жәшігінен қалпына келтіру remove-from-sidebar = Бүйірлік панельден өшіру sort-by-name = Аты бойынша сұрыптау sort-by-modified = Өзгертілген уақыты бойынша сұрыптау sort-by-size = Өлшемі бойынша сұрыптау sort-by-trashed = Өшірілген уақыты бойынша сұрыптау remove-from-recents = Соңғылардан өшіру change-wallpaper = Тұсқағазды өзгерту... desktop-appearance = Жұмыс үстелінің сыртқы түрі... display-settings = Көрсету баптаулары... file = Файл new-tab = Жаңа бет new-window = Жаңа терезе reload-folder = Буманы қайта жүктеу rename = Атын өзгерту... close-tab = Бетті жабу quit = Шығу edit = Түзету cut = Қиып алу copy = Көшіру paste = Кірістіру select-all = Барлығын таңдау zoom-in = Үлкейту default-size = Әдепкі өлшем zoom-out = Кішірейту view = Көрініс grid-view = Тор көрінісі list-view = Тізім көрінісі show-hidden-files = Жасырын файлдарды көрсету list-directories-first = Алдымен бумаларды тізімдеу gallery-preview = Галереяны алдын ала қарау menu-settings = Баптаулар... menu-about = COSMIC файлдар туралы... sort = Сұрыптау sort-a-z = А-Я sort-z-a = Я-А sort-newest-first = Алдымен жаңалары sort-oldest-first = Алдымен ескілері sort-smallest-to-largest = Кішісінен үлкеніне sort-largest-to-smallest = Үлкенінен кішісіне pasted-image = Кірістірілген сурет pasted-text = Кірістірілген мәтін pasted-video = Кірістірілген видео copy-to-title = Көшіру мақсатын таңдаңыз copy-to-button-label = Көшіріп алу move-to-title = Жылжыту мақсатын таңдаңыз move-to-button-label = Жылжыту copy-to = Қайда көшіріп алу... move-to = Қайда жылжыту... comment = COSMIC жұмыс үстелі үшін файлдар басқарушысы keywords = Folder;Manager;Бума;Басқарушы; show-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы clear-recents-history = Жуырдағылар тарихын өшіру copy-path = Орналасқан жолын көшіру mixed = Аралас ================================================ FILE: i18n/kmr/cosmic_files.ftl ================================================ ================================================ FILE: i18n/kn/cosmic_files.ftl ================================================ cosmic-files = ಕಾಸ್ಮಿಕ್ ಫೈಲ್ಸ್ empty-folder = ಖಾಲಿ ಫೋಲ್ಡರ್ empty-folder-hidden = ಖಾಲಿ ಫೋಲ್ಡರ್ (ಗೋಚರವಾಗದ ಐಟಂಗಳನ್ನು ಹೊಂದಿದೆ) no-results = ಯಾವುದೇ ಫಲಿತಾಂಶಗಳು ಇಲ್ಲ filesystem = ಕಡತವ್ಯವಸ್ಥೆ home = ಮನೆ networks = ನೆಟ್ವರ್ಕ್‌ಗಳು notification-in-progress = ಫೈಲ್ ಕಾರ್ಯಾಚರಣೆಗಳು ಪ್ರಗತಿಯಲ್ಲಿದೆ. trash = ಕಸ recents = ಇತ್ತೀಚಿನ undo = ಹಿಂತಿರುಗಿಸು today = ಇಂದು # Desktop view options desktop-view-options = ಡೆಸ್ಕ್‌ಟಾಪ್ ವೀಕ್ಷಣೆಯ ಆಯ್ಕೆಗಳು... show-on-desktop = ಡೆಸ್ಕ್‌ಟಾಪ್‌ನಲ್ಲಿ ತೋರಿಸಿ desktop-folder-content = ಡೆಸ್ಕ್‌ಟಾಪ್ ಫೋಲ್ಡರ್ ವಿಷಯ mounted-drives = ಮೌಂಟ್ ಮಾಡಿರುವ ಡ್ರೈವ್‌ಗಳು trash-folder-icon = ಕಸದ ಫೋಲ್ಡರ್ ಐಕಾನ್ icon-size-and-spacing = ಐಕಾನ್ ಗಾತ್ರ ಮತ್ತು ಅಂತರ icon-size = ಐಕಾನ್ ಗಾತ್ರ # List view name = ಹೆಸರು modified = ಮಾರ್ಪಡಿಸಿದ ದಿನಾಂಕ trashed-on = ಕಸದಲ್ಲಿ ಹಾಕಿದ ದಿನ size = ಗಾತ್ರ # Dialogs ## Compress Dialog create-archive = ಆರ್ಕೈವ್ ರಚಿಸಿ ## Empty Trash Dialog empty-trash = ಕಸ ಖಾಲಿ ಮಾಡಿ empty-trash-warning = ಕಸದಲ್ಲಿನ ಎಲ್ಲಾ ಐಟಂಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಿಸಲು ನೀವು ನಿಜವಾಗಿಯೂ ಬಯಸುತ್ತೀರಾ? ## New File/Folder Dialog create-new-file = ಹೊಸ ಫೈಲ್ ರಚಿಸಿ create-new-folder = ಹೊಸ ಫೋಲ್ಡರ್ ರಚಿಸಿ file-name = ಫೈಲ್ ಹೆಸರು folder-name = ಫೋಲ್ಡರ್ ಹೆಸರು file-already-exists = ಅದೇ ಹೆಸರಿನ ಫೈಲ್ ಈಗಾಗಲೇ ಇದೆ. folder-already-exists = ಅದೇ ಹೆಸರಿನ ಫೋಲ್ಡರ್ ಈಗಾಗಲೇ ಇದೆ. name-hidden = "." ಅಕ್ಷರಗಳಿಂದ ಪ್ರಾರಂಭವಾಗುವ ಹೆಸರುಗಳು ಮರೆಮಾಡಲ್ಪಡುತ್ತವೆ. name-invalid = ಹೆಸರು "{ $filename }" ಆಗಿರಲು ಸಾಧ್ಯವಿಲ್ಲ. name-no-slashes = ಹೆಸರಿನಲ್ಲಿ ಸ್ಲ್ಯಾಶ್‌ಗಳು ಇರಲು ಸಾಧ್ಯವಿಲ್ಲ. ## Open/Save Dialog cancel = ರದ್ದು create = ರಚಿಸಿ open = ತೆರೆಯಿರಿ open-file = ಫೈಲ್ ತೆರೆಯಿರಿ open-folder = ಫೋಲ್ಡರ್ ತೆರೆಯಿರಿ open-in-new-tab = ಹೊಸ ಟ್ಯಾಬ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ open-in-new-window = ಹೊಸ ವಿಂಡೋದಲ್ಲಿ ತೆರೆಯಿರಿ open-item-location = ಐಟಂ ಸ್ಥಳ ತೆರೆಯಿರಿ open-multiple-files = ಬಹು ಫೈಲ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ open-multiple-folders = ಬಹು ಫೋಲ್ಡರ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ save = ಉಳಿಸಿ save-file = ಫೈಲ್ ಉಳಿಸಿ ## Open With Dialog open-with-title = "{ $name }" ಅನ್ನು ಹೇಗೆ ತೆರೆಯಲು ನೀವು ಬಯಸುತ್ತೀರಿ? browse-store = { $store } ಅಲ್ಲಿ ಹುಡುಕಿ ## Rename Dialog rename-file = ಫೈಲ್ ಹೆಸರು ಬದಲಿಸಿ rename-folder = ಫೋಲ್ಡರ್ ಹೆಸರು ಬದಲಿಸಿ ## Replace Dialog replace = ಬದಲಾಯಿಸಿ replace-title = { $filename } ಈಗಾಗಲೇ ಈ ಸ್ಥಳದಲ್ಲಿದೆ. replace-warning = ಉಳಿಸಿರುವ ಫೈಲ್ ಅನ್ನು ಬದಲಾಯಿಸುತ್ತೀರಾ? ಬದಲಾಯಿಸಿದರೆ, ಉಳಿಸಿರುವ ಫೈಲ್ ಮೀರಿಸಿ ಬರೆಯಲಾಗುತ್ತದೆ. replace-warning-operation = ನೀವು ಅದನ್ನು ಬದಲಿಸಲು ಬಯಸುತ್ತೀರಾ? ಬದಲಾಯಿಸಿದರೆ, ಉಳಿಸಿರುವ ಫೈಲ್ ಮೀರಿಸಿ ಬರೆಯಲಾಗುತ್ತದೆ. original-file = ಮೂಲ ಫೈಲ್ replace-with = ಇದರೊಂದಿಗೆ ಬದಲಾಯಿಸಿ apply-to-all = ಎಲ್ಲರಿಗೂ ಅನ್ವಯಿಸಿ keep-both = ಎರಡನ್ನೂ ಇಟ್ಟುಕೊಳ್ಳಿ skip = ಬಿಟ್ಟುಬಿಡಿ ## Set as Executable and Launch Dialog set-executable-and-launch = ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಿ set-executable-and-launch-description = ನೀವು "{ $name }" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಬೇಕೆಂದು ಬಯಸುವಿರಾ? set-and-launch = ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಿ ## Metadata Dialog owner = ಮಾಲೀಕ group = ಗುಂಪು other = ಇತರ # Context Pages ## About ## Add Network Drive add-network-drive = ನೆಟ್ವರ್ಕ್ ಡ್ರೈವ್ ಸೇರಿಸಿ connect = ಸಂಪರ್ಕಿಸಿ connect-anonymously = ಅನಾಮಿಕವಾಗಿ ಸಂಪರ್ಕಿಸಿ connecting = ಸಂಪರ್ಕಿಸುತ್ತಿದೆ... domain = ಡೊಮೇನ್ enter-server-address = ಸರ್ವರ್ ವಿಳಾಸವನ್ನು ನಮೂದಿಸಿ network-drive-description = ಸರ್ವರ್ ವಿಳಾಸಗಳು ಪ್ರೋಟೋಕಾಲ್ ಪೂರ್ವಪ್ರತ್ಯಯ ಮತ್ತು ವಿಳಾಸವನ್ನು ಒಳಗೊಂಡಿರುತ್ತವೆ. ಉದಾಹರಣೆಗಳು: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = ಲಭ್ಯವಿರುವ ಪ್ರೋಟೋಕಾಲ್‌ಗಳು,ಪೂರ್ವಪ್ರತ್ಯಯ AppleTalk,afp:// ಫೈಲ್ ವರ್ಗಾವಣೆ ಪ್ರೋಟೋಕಾಲ್,ftp:// ಅಥವಾ ftps:// ನೆಟ್ವರ್ಕ್ ಫೈಲ್ ಸಿಸ್ಟಮ್,nfs:// ಸರ್ವರ್ ಸಂದೇಶ ಬ್ಲಾಕ್,smb:// SSH ಫೈಲ್ ವರ್ಗಾವಣೆ ಪ್ರೋಟೋಕಾಲ್,sftp:// ಅಥವಾ ssh:// ವೆಬ್ಡಾವ್,dav:// ಅಥವಾ davs:// network-drive-error = ನೆಟ್ವರ್ಕ್ ಡ್ರೈವ್‌ಗೆ ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ password = ಪಾಸ್ವರ್ಡ್ remember-password = ಪಾಸ್ವರ್ಡ್ ನೆನಪಿಡಿ try-again = ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ username = ಬಳಕೆಹೆಸರು ## Operations edit-history = ಸಂಪಾದನೆ ಇತಿಹಾಸ history = ಇತಿಹಾಸ no-history = ಇತಿಹಾಸದಲ್ಲಿ ಐಟಂಗಳಿಲ್ಲ pending = ಬಾಕಿ failed = ವಿಫಲವಾಗಿದೆ complete = ಪೂರ್ಣವಾಗಿದೆ compressing = ಸಂಕುಚಿತ ಮಾಡಲಾಗುತ್ತಿದೆ { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } compressed = ಸಂಕುಚಿತ ಮಾಡಲಾಯಿತು { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } copy_noun = ನಕಲು creating = { $parent } ನಲ್ಲಿ { $name } ರಚಿಸಲಾಗುತ್ತಿದೆ created = { $parent } ನಲ್ಲಿ { $name } ರಚಿಸಲಾಯಿತು copying = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ನಕಲು ಮಾಡಲಾಗುತ್ತಿದೆ copied = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ನಕಲು ಮಾಡಲಾಯಿತು emptying-trash = ಕಸ ಖಾಲಿ ಮಾಡಲಾಗುತ್ತಿದೆ emptied-trash = ಕಸ ಖಾಲಿ ಮಾಡಲಾಯಿತು extracting = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ಹೊರತೆಗೆಯಲಾಗುತ್ತಿದೆ extracted = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ಹೊರತೆಗೆಯಲಾಯಿತು setting-executable-and-launching = "{ $name }" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಲಾಗುತ್ತಿದೆ ಮತ್ತು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ set-executable-and-launched = "{ $name }" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಲಾಗಿದೆ ಮತ್ತು ಪ್ರಾರಂಭಿಸಲಾಗಿದೆ moving = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ಸ್ಥಳಾಂತರಿಸಲಾಗುತ್ತಿದೆ moved = { $items } { $items -> [one] ಐಟಂ *[other] ಐಟಂಗಳು } ನಿಂದ { $from } ಗೆ { $to } ಸ್ಥಳಾಂತರಿಸಲಾಯಿತು renaming = { $from } ನಿಂದ { $to } ಗೆ ಹೆಸರು ಬದಲಾಯಿಸಲಾಗುತ್ತಿದೆ renamed = { $from } ನಿಂದ { $to } ಗೆ ಹೆಸರು ಬದಲಾಯಿಸಲಾಯಿತು restoring = { $items } { $items -> [one] ಐಟಂ     *[other] ಐಟಂಗಳು } ಅನ್ನು ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ restored = { $items } { $items -> [one] ಐಟಂ     *[other] ಐಟಂಗಳು } ಅನ್ನು ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಲಾಯಿತು unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್ ## Open with menu-open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ default-app = { $name } (ಸ್ಥೂಲ) ## Show details show-details = ವಿವರಗಳನ್ನು ತೋರಿಸಿ ## Settings settings = ಸೆಟ್ಟಿಂಗ್‌ಗಳು ### Appearance appearance = ನೋಟ theme = ಥೀಮ್ match-desktop = ಸಿಸ್ಟಮ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ ಅನುಗುಣವಾಗಿ dark = ಡಾರ್ಕ್ light = ಲೈಟ್ # Context menu add-to-sidebar = ಸೈಡ್‌ಬಾರ್‌ಗೆ ಸೇರಿಸಿ compress = ಸಂಕ್ಷಿಪ್ತಗೊಳಿಸಿ extract-here = ಹೊರತೆಗೆಯಿರಿ new-file = ಹೊಸ ಫೈಲ್... new-folder = ಹೊಸ ಫೋಲ್ಡರ್... open-in-terminal = ಟರ್ಮಿನಲ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ move-to-trash = ಕಸಕ್ಕೆ ಸರಿಸಿ restore-from-trash = ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಿ remove-from-sidebar = ಸೈಡ್‌ಬಾರ್‌ನಿಂದ ತೆಗೆಯಿರಿ sort-by-name = ಹೆಸರಿನಿಂದ ವಿಂಗಡಿಸಿ sort-by-modified = ತಿದ್ದುಪಡಿ ಮಾಡಿದ ದಿನಾಂಕದಿಂದ ವಿಂಗಡಿಸಿ sort-by-size = ಗಾತ್ರದಿಂದ ವಿಂಗಡಿಸಿ sort-by-trashed = ಅಳಿಸಿದ ದಿನಾಂಕದಿಂದ ವಿಂಗಡಿಸಿ ## Desktop change-wallpaper = ವಾಲ್‌ಪೇಪರ್ ಬದಲಾಯಿಸಿ... desktop-appearance = ಡೆಸ್ಕ್‌ಟಾಪ್ ಕಾಣಿಕೆ... display-settings = ಡಿಸ್ಪ್ಲೇ ಸೆಟ್ಟಿಂಗ್‌ಗಳು... # Menu ## File file = ಫೈಲ್ new-tab = ಹೊಸ ಟ್ಯಾಬ್ new-window = ಹೊಸ ವಿಂಡೋ rename = ಮರುಹೆಸರು... close-tab = ಟ್ಯಾಬ್‌ನ್ನು ಮುಚ್ಚಿ quit = ಮುಗಿಸಿ ## Edit edit = ಸಂಪಾದಿಸಿ cut = ಕತ್ತರಿಸಿ copy = ನಕಲಿಸಿ paste = ಅಂಟಿಸಿ select-all = ಎಲ್ಲವನ್ನು ಆಯ್ಕೆಮಾಡಿ ## View zoom-in = ಜೂಮ್ ಇನ್ default-size = ಸ್ಥೂಲ ಗಾತ್ರ zoom-out = ಜೂಮ್ ಔಟ್ view = ದೃಶ್ಯ grid-view = ಗ್ರೀಡ್ ವೀಕ್ಷಣೆ list-view = ಪಟ್ಟಿಯ ವೀಕ್ಷಣೆ show-hidden-files = ಮರೆಮಾಡಿದ ಫೈಲ್‌ಗಳನ್ನು ತೋರಿಸಿ list-directories-first = ಡೈರೆಕ್ಟರಿಗಳನ್ನು ಮೊದಲು ತೋರಿಸಿ menu-settings = ಸೆಟ್ಟಿಂಗ್‌ಗಳು... menu-about = COSMIC ಫೈಲ್ ಬಗ್ಗೆ... ## Sort sort = ವಿಂಗಡಿಸಿ sort-a-z = ಅ-ಆ ಕ್ರಮದಲ್ಲಿ ವಿಂಗಡಿಸಿ sort-z-a = ಆ-ಅ ಕ್ರಮದಲ್ಲಿ ವಿಂಗಡಿಸಿ sort-newest-first = ಹೊಸದರಿಂದ ಹಳೆಯದು sort-oldest-first = ಹಳೆಯದರಿಂದ ಹೊಸದು sort-smallest-to-largest = ಚಿಕ್ಕದರಿಂದ ದೊಡ್ಡದು sort-largest-to-smallest = ದೊಡ್ಡದರಿಂದ ಚಿಕ್ಕದು ================================================ FILE: i18n/ko/cosmic_files.ftl ================================================ cosmic-files = COSMIC 파일 empty-folder = 빈 폴더 empty-folder-hidden = 빈 폴더 (숨겨진 항목 있음) filesystem = 파일 시스템 home = 홈 trash = 휴지통 # New File/Folder Dialog create-new-file = 새 파일 만들기 create-new-folder = 새 폴더 만들기 file-name = 파일 이름 folder-name = 폴더 이름 file-already-exists = 같은 이름의 파일이 이미 있습니다 folder-already-exists = 같은 이름의 폴더가 이미 있습니다 name-hidden = "." 으로 시작하는 항목은 숨겨집니다 name-invalid = "{ $filename }"은(는) 사용할 수 없는 이름입니다 name-no-slashes = 이름에 슬래시(/)를 사용할 수 없습니다 # Open/Save Dialog cancel = 취소 open = 열기 open-file = 파일 열기 open-folder = 폴더 열기 open-multiple-files = 여러 파일 열기 open-multiple-folders = 여러 폴더 열기 save = 저장 save-file = 파일 저장 # Rename Dialog rename-file = 파일 이름 바꾸기 rename-folder = 폴더 이름 바꾸기 # Replace Dialog replace = 대체 replace-title = "{ $filename }" 이(가) 이미 현재 위치에 있습니다 replace-warning = 저장하려는 파일로 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다. # List view name = 이름 modified = 수정된 날짜 size = 크기 # Context Pages ## About ## Operations pending = 진행 중 failed = 실패함 complete = 완료 ## Open with menu-open-with = 다른 앱으로 열기 default-app = { $name } (기본) ## Properties ## Settings settings = 설정 ### Appearance appearance = 외관 theme = 테마 match-desktop = 데스크톱에 맞춤 dark = 다크 light = 라이트 # Context menu new-file = 새 파일... new-folder = 새 폴더... open-in-terminal = 터미널에서 열기 move-to-trash = 휴지통으로 이동 restore-from-trash = 휴지통에서 복구 sort-by-name = 파일명으로 정렬 sort-by-modified = 수정된 날짜 순으로 정렬 sort-by-size = 크기 순으로 정렬 # Menu ## File file = 파일 new-tab = 새 탭 new-window = 새 창 rename = 이름 바꾸기... close-tab = 탭 닫기 quit = 종료 ## Edit edit = 편집 cut = 잘라내기 copy = 복사 paste = 붙여넣기 select-all = 모두 선택 ## View view = 보기 grid-view = 그리드 보기 list-view = 목록 보기 menu-settings = 설정... menu-about = COSMIC 파일 정보... connect = 연결 read-execute = 읽기 및 실행 item-modified = 마지막 수정 일자: { $modified } dismiss = 메시지 무시 copy_noun = 복사 progress = { $percent }% related-apps = 관련 앱 compress = 압축... network-drive-error = 네트워크 드라이브에 접근할 수 없음 icon-size-and-spacing = 아이콘 크기 및 간격 password = 암호 type-to-search-enter-path = 폴더 혹은 파일의 경로 입력 emptying-trash = { trash } 비우는 중 ({ $progress })... trashed-on = 버려짐 remove = 제거 original-file = 원본 파일 create = 생성 create-archive = 압축 생성 read-write-execute = 읽기, 쓰기 및 실행 other-apps = 다른 앱 set-permissions = "{ $name }"의 권한을 { $mode }로 설정함 pause = 정지 calculating = 계산 중... keep = 유지 item-size = 크기: { $size } connecting = 연결 중... read-write = 읽기 및 쓰기 none = 없음 items = 항목: { $items } no-results = 결과 없음 type = 형식: { $mime } resume = 재개 remember-password = 암호 저장 username = 사용자 이름 show-details = 세부 사항 표시 extract-to = 다른 위치에 압축 해제... add-network-drive = 네트워크 드라이브 추가 delete = 삭제 repository = 저장소 replace-warning-operation = 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다. support = 지원 try-again = 다시 시도 eject = 꺼내기 other = 기타 open-in-new-window = 새 창에서 열기 read-only = 읽기 전용 browse-store = { $store } 둘러보기 enter-server-address = 서버 주소 입력 connect-anonymously = 익명으로 연결 group = 그룹 apply-to-all = 모두 적용 skip = 건너뛰기 replace-with = 대체할 파일 recents = 최근 network-drive-description = 서버 주소는 프로토콜 접두어와 주소를 포함해야 합니다. 예시: ssh://192.168.0.1, ftp://[2001:db8::1] single-click = 클릭 한 번으로 열기 undo = 되돌리기 setting-permissions = "{ $name }"의 권한을 { $mode }로 설정 중 owner = 소유자 creating = "{ $parent }"에 "{ $name }" 생성 중 execute-only = 실행 전용 open-item-location = 항목 위치 열기 details = 세부 정보 mounted-drives = 마운트된 드라이브 mount-error = 드라이브에 접근할 수 없음 extract-here = 압축 해제 removed-from-recents = { recents } 에서 { $items }개의 항목을 제거했습니다 add-to-sidebar = 사이드 바에 추가 item-created = 생성 일자: { $created } type-to-search-recursive = 현재 폴더와 하위 폴더 탐색 history = 기록 progress-paused = { $percent }%, 정지됨 desktop-view-options = 바탕화면 표시 설정... show-on-desktop = 바탕화면에 표시 cancelled = 취소됨 domain = 도메인 edit-history = 기록 수정 progress-failed = { $percent }%, 실패함 item-accessed = 마지막 접근 일자: { $accessed } extract-to-title = 폴더로 압축 해제 open-with = 다음으로 열기 keep-both = 둘 다 유지 icon-size = 아이콘 크기 open-with-title = "{ $name }"을(를) 어떻게 열까요? write-execute = 쓰기 및 실행 extract-password-required = 암호 필요 desktop-folder-content = 바탕화면 폴더 내용 no-history = 기록된 항목이 없습니다. emptied-trash = { trash } 비워짐 progress-cancelled = { $percent }%, 취소됨 open-in-new-tab = 새 탭에서 열기 unknown-folder = 알 수 없는 폴더 created = "{ $parent }"에 "{ $name }" 생성됨 delete-permanently = 완전히 삭제 networks = 네트워크 write-only = 쓰기 전용 today = 오늘 permanently-delete-warning = { $target } 이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다. empty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다 empty-trash = 휴지통 비우기 empty-trash-title = 휴지통을 비울까요? type-to-search = 입력하여 검색 notification-in-progress = 파일 작업이 진행 중입니다 permanently-delete-question = 완전히 삭제할까요? selected-items = { $items }개 항목 선택됨 sort-newest-first = 새 항목 우선 renamed = "{ $from }" 에서 "{ $to }" 로 이름 변경됨 deleted = { trash } 에서 { $items }개의 항목을 제거했습니다 reload-folder = 폴더 새로고침 favorite-path-error = 디렉터리를 여는 중 오류가 발생했습니다 remove-from-sidebar = 사이드 바에서 제거 restoring = { trash } 에서 { $items }개의 항목을 복구 중 ({ $progress })... gallery-preview = 갤러리 미리보기 sort-smallest-to-largest = 작은 항목부터 큰 항목 zoom-in = 확대 removing-from-recents = { recents } 에서 { $items }개의 항목을 제거 중 zoom-out = 축소 compressing = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 중({ $progress })... setting-executable-and-launching = "{ $name }"를 실행 가능으로 설정 및 실행 중 default-size = 기본 크기 extracted = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 해제했습니다 permanently-deleting = { $items }개의 항목을 영구적으로 제거 중 compressed = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축했습니다 grid-spacing = 그리드 간격 copying = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 복사 중({ $progress })... sort-oldest-first = 오래된 항목 우선 sort-by-trashed = 삭제된 시간 순으로 정렬 copied = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 복사했습니다 list-directories-first = 폴더 우선 나열 remove-from-recents = 최근 항목에서 제거 moving = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 이동 중 ({ $progress })... change-wallpaper = 배경화면 변경... deleting = { trash } 에서 { $items }개의 항목을 제거 중({ $progress })... set-executable-and-launched = "{ $name }"를 실행 가능으로 설정 및 실행됨 sort-a-z = A-Z set-and-launch = 설정 후 실행 set-executable-and-launch = 실행 가능으로 설정 후 실행 restored = { trash } 에서 { $items }개의 항목을 복구했습니다 sort-z-a = Z-A operations-running-finished = { $running }개의 작업 진행 중 ({ $percent }%), { $finished } 완료됨... sort = 정렬 show-hidden-files = 숨긴 파일 표시 trash-folder-icon = 휴지통 아이콘 extracting = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 해제 중 ({ $progress })... permanently-deleted = { $items }개의 항목을 영구적으로 제거했습니다 renaming = "{ $from }" 에서 "{ $to }" 로 이름 변경 중 set-executable-and-launch-description = "{ $name }"을 실행 가능으로 설정하고 실행할까요? sort-largest-to-smallest = 큰 항목부터 작은 항목 moved = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 이동했습니다 display-settings = 화면 설정... desktop-appearance = 데스크톱 외관... favorite-path-error-description = "{ $path }"을(를) 열 수 없습니다 "{ $path }"이(가) 존재하지 않거나 열기 권한이 없을 수 있습니다 사이드바에서 제거하시겠습니까? operations-running = { $running }개의 작업 진행 중 ({ $percent }%)... network-drive-schemes = 지원 프로토콜,접두사(Prefix) AppleTalk,afp:// 파일 전송 프로토콜 (FTP),ftp:// 또는 ftps:// 네트워크 파일 시스템 (NFS),nfs:// 서버 메시지 블록 (SMB),smb:// SSH 파일 전송 프로토콜 (SFTP),sftp:// 또는 ssh:// WebDAV,dav:// 또는 davs:// type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다 comment = COSMIC 데스크톱용 위한 파일 관리자 keywords = 폴더;관리자; copy-to-button-label = 복사 move-to-button-label = 이동 clear-recents-history = 최근 기록 비우기 copy-path = 복사 경로 ================================================ FILE: i18n/li/cosmic_files.ftl ================================================ ================================================ FILE: i18n/lt/cosmic_files.ftl ================================================ progress = { $percent }% cosmic-files = Cosmic Files empty-folder = Tuščias aplankas empty-folder-hidden = Tuščias aplankas (turi paslėptų failų) no-results = Rezultatų nėra filesystem = Failų sistema home = Namai networks = Tinklai notification-in-progress = Vyksta failų operacijos trash = Šiukšlinė recents = Neseniai naudoti undo = Anuliuoti today = Šiandien desktop-view-options = Darbalaukio peržiūros parinktys... show-on-desktop = Rodyti darbalaukyje desktop-folder-content = Darbalaukio aplanko turinys mounted-drives = Prijungti kaupikliai trash-folder-icon = Šiukšlinės aplanko piktograma icon-size-and-spacing = Piktogramos dydis ir tarpai icon-size = Piktogramos dydis grid-spacing = Tinklelio tarpai name = Pavadinimas modified = Modifikuota trashed-on = Ištrinta size = Dydis details = Išsami informacija dismiss = Atmesti pranešimą operations-running = { $running } { $running -> [one] vykdoma operaciją *[other] vykdomos operacijos } ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] vykdoma operacija *[other] vykdomos operacijos } ({ $percent }%), { $finished } baigtos... pause = Pauzė resume = Tęsti create-archive = Sukurti archyvą extract-password-required = Reikalingas slaptažodis extract-to = Išskleistį į... extract-to-title = Išskleisti į aplanką empty-trash-title = Ištuštinti šiukšlinę? empty-trash-warning = Šiukšlinjė esantys elementai bus ištrinti negrįžtamai mount-error = Nepavyko pasiekti kaupyklės create-new-file = Sukurti naują failą create-new-folder = Sukurti naują aplanką file-name = Failo pavadinimas folder-name = Aplanko pavadinimas file-already-exists = Failas su tokiu vardu jau egzistuoja empty-trash = Ištuštinti šiukšlinę folder-already-exists = Aplankas su tokiu vardu jau egzistuoja name-hidden = Pavadinimai prasidedantys „.“ simboliu bus paslėpti name-invalid = „{ $filename }“ negalima naudoti kaip pavadinimo name-no-slashes = Pavadinime negali būti pasvyrųjų brūkšnių cancel = Atšaukti create = Sukurti open = Atidaryti open-file = Atidaryti failą open-folder = Atidaryti aplanką open-in-new-tab = Atidaryti naujame skirtuke open-in-new-window = Atidaryti naujame lange open-item-location = Atidaryti elemento vietą open-multiple-files = Atidaryti keletą failų open-multiple-folders = Atidaryti keletą aplankų save = Išsaugoti save-file = Išsaugoti failą open-with-title = Kaip norite atidaryti „{ $name }“? browse-store = Naršyti { $store } other-apps = Kitos aplikacijos related-apps = Susijusios aplikacijos selected-items = Pažymėti { $items } elementai permanently-delete-question = Ištrinti visam laikui? 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-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. original-file = Originalus failas replace-with = Pakeisti su apply-to-all = Pritaikyti visiems keep-both = Palikti abu skip = Praleisti set-executable-and-launch = Nustatyti kaip paleidžiamą ir paleisti set-executable-and-launch-description = Ar norite nustatyti „{ $name }“ kaip paleidžiamą ir paleisti iš karto? set-and-launch = Nustatyti ir paleisti open-with = Atidaryti su owner = Savininkas group = Grupė other = Kita none = Joks execute-only = Tik paleidžiamas write-only = Tik įrašomas write-execute = Įrašyti ir paleisti read-only = Tik skaitomas read-execute = Skaitomas ir paleidžiamas read-write = Skaitomas ir įrašomas read-write-execute = Skaitomas, įrašomas ir paleidžiamas favorite-path-error = Klaida atidarant aplanką favorite-path-error-description = Nepavyko atidaryt „{ $path }“ „{ $path }“ gali neegzistuoti arba neturi teisių atidaryti Ar norėtumėte pašalinti tai iš šonjuostės? remove = Pašalinti keep = Išlaikyti repository = Saugykla support = Palaikymas add-network-drive = Pridėti tinklo talpyklą connect = Prijungti connect-anonymously = Prijungti anonimiškai connecting = Jungiamasi... domain = Domenas enter-server-address = Įveskite serverio adresą network-drive-description = Serverio adresą sudaro protokolas ir adresas. Pavyzdžiui: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Galimi protokolai, Prefiksas AppleTalk, afp:// Duomenų Perdavimo Protokolas (File Transfer Protocol), ftp:// or ftps:// Tinklo Failų Sistema (Network File System), nfs:// Serverio Žinučių Blokas (Server Message Block), smb:// SSH Duomenų Perdavimo Protokolas, sftp:// or ssh:// WebDAV, dav:// or davs:// network-drive-error = Nepavyko pasiekti tinklo kaupyklos password = Slaptažodis remember-password = Prisiminti slaptažodį try-again = Pabandyti dar kartą username = Naudotojo vardas cancelled = Atšaukta edit-history = Redaguoti istoriją history = Istorija no-history = Istorija tuščia. pending = Laukiama patvirtinimo progress-cancelled = { $percent }%, atšaukta progress-failed = { $percent }%, nepavyko progress-paused = { $percent }%, pristabdyta failed = Nepavyko complete = Baigta compressing = Suspaudžiami { $items } { $items -> [one] elementas *[other] elementai } iš „{ $from }“ į „{ $to }“ ({ $progress })... compressed = { $items } { $items -> [one] Suspaustas elementas *[other] Suspausti elementai } iš „{ $from }“ to „{ $to }“ copy_noun = Kopijuoti creating = Kuriamas „{ $name }“ „{ $parent }“ lokacijoje created = Sukurtas „{ $name }“ „{ $parent }“ lokacijoje copying = { $items } { $items -> [one] Kopijuojamas elementas *[other] Kopijuojami elementai } iš „{ $from }“ į „{ $to }“ ({ $progress })... copied = { $items } { $items -> [one] Nukopijuotas elementas *[other] Nukopijuoti elementai } iš „{ $from }“ į „{ $to }“ deleting = { $items } { $items -> [one] Trinamas elementas *[other] Trinami elementai } iš { trash } { $progress }... emptying-trash = Tuštinama { trash } ({ $progress })... deleted = { $items } { $items -> [one] Ištrintas elementas *[other] Ištrinti elementai } iš { trash } emptied-trash = Ištuštinta { trash } extracting = { $items } { $items -> [one] Išskleidžiamas elementas *[other] Išskleidžiami elementai } iš „{ $from }“ į „{ $to }“ { $progress }... extracted = { $items } { $items -> [one] Iškleistas elementas *[other] Iškleisti elementai } iš „{ $from }“ į „{ $to }“ setting-executable-and-launching = Nustatomas „{ $name }“, kaip vykdomas, ir paleidžiamas set-executable-and-launched = Nustatyti „{ $name }“, kaip vykdomą, ir paleisti setting-permissions = Nustatomi leidimai { $mode } "{ $name }" set-permissions = Nustatyti { $mode } leidimus „{ $name }“ moving = { $items } { $items -> [one] Perkeliamas elementas *[other] Perkeliami elementai } iš „{ $from }“ į „{ $to }“ { $progress }... moved = { $items } { $items -> [one] Perkeltas elementas *[other] Perkelti elementai } iš „{ $from }“ į „{ $to }“ permanently-deleting = { $items } { $items -> [one] Visam laikui ištrinamas elementas *[other] Visam laikui ištrinami elementai } permanently-deleted = { $items } { $items -> [one] Visam laikui ištrintas elementas *[other] Visam laikui ištrinti elementai } removing-from-recents = { $items } { $items -> [one] Pašalinamas elementas *[other] Pašalinami elementi } iš { recents } removed-from-recents = { $items } { $items -> [one] Pašalintas elementas *[other] Pašalinti elementai } iš { recents } renaming = Pervadinamas „{ $from }“ į „{ $to }“ renamed = „{ $from }“ pervadintas į „{ $to }“ restoring = { $items } { $items -> [one] Atkuriamas elementas *[other] Atkuriami elementai } iš { trash } ({ $progress })... restored = { $items } { $items -> [one] Atkurtas elementas *[other] Atkurti elementai } iš { trash } unknown-folder = nežinomas aplankas menu-open-with = Atidaryti su... default-app = { $name } (numatytas) show-details = Rodyti išsamią informaciją type = Tipas: { $mime } items = Elementai: { $items } item-size = Dydis: { $size } item-created = Sukurtas: { $created } item-modified = Modifikuota: { $modified } item-accessed = Paskutinė prieiga: { $accessed } calculating = Skaičiuojama... settings = Nustatymai single-click = Vieno paspaudimo atidarymas appearance = Išvaizda match-desktop = Pagal darbalaukio temą 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... delete-permanently = Ištrinti visam laikui eject = Išstumti extract-here = Išskleisti new-file = Naujas failas... new-folder = Naujas aplankas... open-in-terminal = Atidaryti terminale move-to-trash = Perkelti į šiukšlinę restore-from-trash = Atkurti iš šiukšlinės remove-from-sidebar = Pašalinti iš šonjuostės sort-by-name = Rikiuoti pagal pavadinimą sort-by-modified = Rikiuoti pagal modifikavimo laiką sort-by-size = Rikiuoti pagal dydį sort-by-trashed = Rikiuoti pagal ištrinimo laiką remove-from-recents = Pašalinti iš neseniai naudotų change-wallpaper = Pakeisti darbalaukio foną... desktop-appearance = Darbalaukio išvaizda... display-settings = Vaizdo nustatymai... file = Failas new-tab = Naujas skirtukas new-window = Naujas langas reload-folder = Perkrauti aplanką rename = Pervadinti... close-tab = Uždaryti skirtuką quit = Išeiti edit = Redaguoti cut = Iškirpti copy = Kopijuoti paste = Įklijuoti select-all = Pažymėti viską zoom-in = Priartinti default-size = Numatytas dydis zoom-out = Nutolinti 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-about = Apie COSMIC Files... sort = Rikiuoti sort-a-z = A-Ž sort-z-a = Ž-A sort-newest-first = Pirma naujausi sort-oldest-first = Pirma seniausi 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ė keywords = Aplankas;Tvarkyklė; copy-to-title = Pasirinkti kopijavimo vietą copy-to-button-label = Kopijuoti move-to-title = Pasirinkti perkėlimo vietą move-to-button-label = Perkelti pasted-image = Įklijuotas Atvaizdas pasted-text = Įklijuotas Tekstas pasted-video = Įklijuotas Vaizdo įrašas show-recents = Neseniai naudotų aplankas šonjuostėje type-to-search-select = Parenkamas pirmas atitinkantis failas arba aplankas clear-recents-history = Išvalyti Neseniai naudotų istoriją copy-to = Kopijuoti į... move-to = Perkeltiį į... copy-path = Kopijuoti kelią ================================================ FILE: i18n/ml/cosmic_files.ftl ================================================ ================================================ FILE: i18n/ms/cosmic_files.ftl ================================================ ================================================ FILE: i18n/nb-NO/cosmic_files.ftl ================================================ open-file = Åpne fil open-folder = Åpne mappe settings = Innstillinger appearance = Utseende theme = Tema dark = Mørk light = Lys file = Fil quit = Avslutt cosmic-files = COSMIC Filer empty-folder = Tom mappe cancel = Avbryt empty-folder-hidden = Tom mappe (har skjulte filer) no-results = Ingen resultater support = Støtte filesystem = Filsystem home = Hjem networks = Nettverk notification-in-progress = Filoperasjoner pågår. delete = Slett trash = Papirkurv recents = Nylige connect = Koble til undo = Angre match-desktop = Følg skrivebordet open = Åpne today = I dag name = Navn desktop-view-options = Visningsalternativer for skrivebord… show-on-desktop = Vis på skrivebord desktop-folder-content = Innhold i skrivebordsmappe mounted-drives = Monterte disker trash-folder-icon = Papirkurvikon password = Passord remove = Fjern username = Brukernavn icon-size-and-spacing = Ikonstørrelse og avstand icon-size = Ikonstørrelse grid-spacing = Rutenettavstand size = Størrelse details = Detaljer dismiss = Avvis beskjed operations-running = { $running } { $running -> [one] operasjon *[other] operasjoner } pågår ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operasjon *[other] operasjoner } pågår ({ $percent }%), { $finished } ferdige... pause = Pause save = Lagre repository = Kodelager modified = Modifisert trashed-on = Slettet resume = Fortsett create-archive = Opprett arkiv extract-password-required = Passord kreves extract-to = Pakk ut til... extract-to-title = Pakk ut i mappe new-tab = Ny fane new-window = Nytt vindu close-tab = Lukk fane edit = Rediger copy = Kopier paste = Lim inn select-all = Velg alt view = Vis menu-settings = Innstillinger... folder-name = Mappenavn replace = Erstatt file-already-exists = En fil med det navnet finnes allerede. folder-already-exists = En mappe med det navnet finnes allerede. name-hidden = Navn som begynner med "." vil være skjult. open-in-new-window = Åpne i nytt vindu empty-trash = Tøm papirkurven empty-trash-warning = Er du sikker på at du vil slette alle filene i papirkurven permanent? mount-error = Kunne ikke koble til disken create-new-file = Lag ny fil create-new-folder = Lag ny mappe file-name = Filnavn name-invalid = Navn kan ikke være "{ $filename }". name-no-slashes = Navn kan ikke inneholde skråstrek. create = Skape open-in-new-tab = Åpne i ny fane open-item-location = Åpne filplassering open-multiple-files = Åpne flere filer open-multiple-folders = Åpne flere mapper save-file = Lagre fil open-with-title = Hvordan vil du åpne "{ $name }"? browse-store = Bla i { $store } other-apps = Andre applikasjoner related-apps = Relaterte applikasjoner selected-items = de { $items } valgte objektene permanently-delete-question = Slett permanent permanently-delete-warning = Er du sukker på om du vil permanent slette { $target }? Dette kan ikke angres. rename-file = Endre filnavn rename-folder = Endre mappenavn replace-title = "{ $filename }" finnes allerede på denne plasseringen. replace-warning = Vil du bytte den ut med filen du lagrer? Dette vil overskrive innholdet. replace-warning-operation = Vil du erstatte den? Dette vil overskrive innholdet. original-file = Orginalfil replace-with = Erstatt med apply-to-all = Bruk på alle keep-both = Behold begge skip = Hopp over set-executable-and-launch = Gjør kjørbar og start set-executable-and-launch-description = Vil du gjøre «{ $name }» kjørbar og starte den? open-with = Åpne med owner = Eier group = Gruppe other = Andre none = Ingen execute-only = Kun kjørbar write-only = Kun skrivbar write-execute = Kun skrive- og kjørbar read-only = Kun lesbar read-execute = Kun lese- og kjørbar read-write = Kun skrive- og lesbar read-write-execute = Lesbar, skrivbar og kjørbar favorite-path-error = Feil ved åpning av mappen favorite-path-error-description = Kunne ikke åpne "{ $path }". Den kan ikke eksistere eller så har du ikke tilgang til å åpne den. Vil du fjerne den fra sidepanelet? keep = Behold add-network-drive = Legg til nettverksdisk connect-anonymously = Koble til anonymt connecting = Kobler til... cancelled = Avbrutt pending = Venter failed = Mislykkede complete = Ferdige zoom-in = Zoom inn default-size = Standard størrelse zoom-out = Zoom ut cut = Klipp ut set-and-launch = Still inn og start domain = Domene enter-server-address = Angi serveradresse network-drive-description = Serveradresser inkluderer ett protokollprefiks og en addresse. Examples: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Tilgjengelige protokoller, Prefiks AppleTalk,afp:// File Transfer Protocol,ftp:// eller ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// eller ssh:// WebDAV,dav:// eller davs:// network-drive-error = Kunne ikke nå nettverksdisk remember-password = Husk passord try-again = Prøv igjen edit-history = Rediger hisorikk history = Historikk no-history = Ingenting i historikken. progress = { $percent }% progress-cancelled = { $percent }%, avbrutt progress-failed = { $percent }%, mislykket progress-paused = { $percent }%, satt på pause compressing = Komprimerer { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... compressed = Komprimerete { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" copy_noun = Kopi creating = Oppretter "{ $name }" i "{ $parent }" created = Opprettet "{ $name }" i "{ $parent }" copying = Kopierer { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" ({ $progress })... copied = Kopierete { $items } { $items -> [one] objekt *[other] objekter } fra "{ $from }" til "{ $to }" deleting = Sletter { $items } { $items -> [one] objekt *[other] objekt } fra { trash } ({ $progress })... deleted = Slettet { $items } { $items -> [one] objekt *[other] objekter } fra { trash } emptying-trash = Tømmer { trash } ({ $progress })... emptied-trash = Tømte { trash } extracting = Pakker ut { $items } { $items -> [one] objekt *[other] objekter } fra «{ $from }» til «{ $to }» ({ $progress })... extracted = Pakka ut { $items } { $items -> [one] objekt *[other] objekter } fra «{ $from }» til «{ $to }» setting-executable-and-launching = Setter «{ $name }» som kjørbar og starter set-executable-and-launched = Sett «{ $name }» som kjørbar og startet setting-permissions = Setter tillatelser for «{ $name }» til { $mode } set-permissions = Sett tillatelser for «{ $name }» til { $mode } moving = Flytter { $items } { $items -> [one] objekt *[other] objekter } fra «{ $from }» til «{ $to }» ({ $progress })... moved = Flytta { $items } { $items -> [one] objekt *[other] objekter } fra «{ $from }» til «{ $to }» permanently-deleting = Sletter { $items } { $items -> [one] objekt *[other] objekter } permanent permanently-deleted = Sletta { $items } { $items -> [one] objekt *[other] objekter } permanent removing-from-recents = Fjerner { $items } { $items -> [one] objekt *[other] objekter } fra { recents } removed-from-recents = Fjerna { $items } { $items -> [one] objekt *[other] objekter } fra { recents } renaming = Bytter navn «{ $from }» til «{ $to }» renamed = Byttet navn «{ $from }» til «{ $to }» restoring = Gjenopretter { $items } { $items -> [one] objekt *[other] objekter } fra { trash } ({ $progress })... restored = Gjenopretta { $items } { $items -> [one] objekt *[other] objekter } fra { trash } unknown-folder = ukjent mappe menu-open-with = Åpne med… default-app = { $name } (standard) show-details = Vis detaljer type = Type: { $mime } items = Objekter: { $items } item-size = Størrelse: { $size } item-created = Opprettet: { $created } item-modified = Endret: { $modified } item-accessed = Åpnet: { $accessed } calculating = Beregner... single-click = Ett klikk for å åpne type-to-search = Skriv for å Søke type-to-search-recursive = Søker i den aktuelle mappen og alle undermapper type-to-search-enter-path = Åpner plasseringen til mappen eller filen add-to-sidebar = Legg til sidepanelet compress = Komprimer delete-permanently = Slett permanent eject = Løs ut extract-here = Pakk ut new-file = Ny fil… new-folder = Ny mappe… open-in-terminal = Åpne i terminal move-to-trash = Flytt til papirkurven restore-from-trash = Gjenoprett fra papirkurven remove-from-sidebar = Fjern fra sidepanelet sort-by-name = Sorter etter navn sort-by-modified = Sorter etter modifisert sort-by-size = Sorter etter størrelse sort-by-trashed = Sorter etter slettingsdato remove-from-recents = Fjern fra nylige change-wallpaper = Bytt bakgrunnsbilde… desktop-appearance = Skrivebordsutseende... display-settings = Skjerminnstillinger… reload-folder = Last inn mappe på nytt rename = Gi nytt navn… grid-view = Rutevisning list-view = Rutenettvisning show-hidden-files = Vis skjulte filer list-directories-first = List mapper først gallery-preview = Galleriforhåndsvisning menu-about = Om COSMIC Filer... sort = Sorter sort-a-z = A-Å sort-z-a = Å-A sort-newest-first = Nyeste først sort-oldest-first = Eldste først sort-smallest-to-largest = Minste til største sort-largest-to-smallest = Største til minste ================================================ FILE: i18n/nl/cosmic_files.ftl ================================================ cosmic-files = COSMIC Bestanden empty-folder = Lege map empty-folder-hidden = Lege map (met verborgen items) no-results = Geen resultaten gevonden filesystem = Bestandssysteem home = Persoonlijke map networks = Netwerken notification-in-progress = Bestandsbewerkingen worden uitgevoerd trash = Prullenbak recents = Recente bestanden undo = Ongedaan maken today = Vandaag # Desktop view options desktop-view-options = Opties voor bureaubladweergave… show-on-desktop = Op bureaublad weergeven desktop-folder-content = Bestanden in de Bureablad-map mounted-drives = Aangekoppelde schijven trash-folder-icon = Pictogram van de Prullenbak-map icon-size-and-spacing = Pictogramgrootte en -afstand icon-size = Pictogramgrootte grid-spacing = Rasterafstand # List view name = Naam modified = Gewijzigd trashed-on = Verwijderd op size = Grootte # Progress footer details = Details dismiss = Bericht negeren operations-running = { $running } { $running -> [one] bewerking wordt *[other] bewerkingen worden } uitgevoerd ({ $percent }%) … operations-running-finished = { $running } { $running -> [one] bewerking wordt *[other] bewerkingen worden } uitgevoerd ({ $percent }%), { $finished } voltooid... pause = Pauzeren resume = Hervatten # Dialogs ## Compress Dialog create-archive = Archief aanmaken ## Extract Dialog extract-password-required = Wachtwoord vereist extract-to = Uitpakken naar… extract-to-title = Uitpakken naar map ## Empty Trash Dialog empty-trash = Prullenbak leegmaken empty-trash-warning = Bestanden in de Prullenbak-map worden permanent verwijderd ## Mount Error Dialog mount-error = Geen toegang tot schijf ## New File/Folder Dialog create-new-file = Nieuw bestand aanmaken create-new-folder = Nieuwe map aanmaken file-name = Bestandsnaam folder-name = Mapnaam file-already-exists = Er bestaat al een bestand met deze naam folder-already-exists = Er bestaat al een map met deze naam name-hidden = Namen die met “.” beginnen worden verborgen name-invalid = Ongeldige naam “{ $filename }” name-no-slashes = Naam mag geen schuine strepen bevatten ## Open/Save Dialog cancel = Annuleren create = Aanmaken open = Openen open-file = Bestand openen open-folder = Map openen open-in-new-tab = In nieuw tabblad openen open-in-new-window = In nieuw venster openen open-item-location = Bestandslocatie openen open-multiple-files = Meerdere bestanden openen open-multiple-folders = Meerdere mappen openen save = Opslaan save-file = Bestand opslaan ## Open With Dialog open-with-title = Hoe wilt u “{ $name }” openen? browse-store = { $store } verkennen other-apps = Andere toepassingen related-apps = Gerelateerde toepassingen ## Permanently delete Dialog selected-items = De { $items } geselecteerde items permanently-delete-question = Permanent verwijderen? delete = Verwijderen permanently-delete-warning = { $target } wordt permanent verwijderd. Dit kan niet ongedaan gemaakt worden. ## Rename Dialog rename-file = Bestand hernoemen rename-folder = Map hernoemen ## Replace Dialog replace = Vervangen replace-title = “{ $filename }” bestaat al op deze locatie replace-warning = Wilt u het bestaande bestand vervangen? Als u het vervangt, wordt de inhoud ervan overschreven. replace-warning-operation = Wilt u het vervangen? Dit kan niet ongedaan gemaakt worden. original-file = Oorspronkelijk bestand replace-with = Vervangen door apply-to-all = Op alles toepassen keep-both = Beide behouden skip = Overslaan ## Set as Executable and Launch Dialog set-executable-and-launch = Als uitvoerbaar instellen en dan starten set-executable-and-launch-description = Wilt u “{ $name }” als uitvoerbaar instellen en dan starten? set-and-launch = Uitvoerbaar maken en starten ## Metadata Dialog open-with = Openen met owner = Eigenaar group = Groep other = Anderen ### Mode 0 none = Geen ### Mode 1 (unusual) execute-only = Alleen uitvoeren ### Mode 2 (unusual) write-only = Alleen schrijven ### Mode 3 (unusual) write-execute = Schijven en uitvoeren ### Mode 4 read-only = Alleen lezen ### Mode 5 read-execute = Lezen en uitvoeren ### Mode 6 read-write = Lezen en schrijven ### Mode 7 read-write-execute = Lezen, schrijven en uitvoeren ## Favorite Path Error Dialog favorite-path-error = Fout bij het openen van de map favorite-path-error-description = Kon de map '{ $path }' niet openen. De map bestaat mogelijk niet of u heeft geen toestemming om die te openen. Wilt u de map uit de favorieten verwijderen? remove = Verwijderen keep = Behouden # Context Pages ## About ## Add Network Drive add-network-drive = Netwerkschijf toevoegen connect = Verbinden connect-anonymously = Anoniem verbinden connecting = Verbinding maken… domain = Domein enter-server-address = Serveradres invoeren network-drive-description = Serveradressen bestaan uit protocolvoorvoegsel en netwerkadres. Voorbeelden: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Beschikbare protocollen,Voorvoegsel AppleTalk,afp:// File Transfer Protocol,ftp:// of ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// of ssh:// WebDav,dav:// of davs:// network-drive-error = Geen toegang tot de netwerkschijf password = Wachtwoord remember-password = Wachtwoord onthouden try-again = Opnieuw proberen username = Gebruikersnaam ## Operations cancelled = Geannuleerd edit-history = Geschiedenis bewerken history = Geschiedenis no-history = Geen items in de geschiedenis. pending = In afwachting progress = { $percent }% progress-cancelled = { $percent }%, geannuleerd progress-paused = { $percent }%, gepauzeerd failed = Mislukt complete = Voltooid compressing = { $items } { $items -> [one] bestand *[other] bestanden } van '{ $from }' naar '{ $to }' comprimeren ({ $progress })... compressed = { $items } { $items -> [one] bestand *[other] bestanden } gecomprimeerd van '{ $from }' naar '{ $to }' copy_noun = Kopie creating = '{ $name }' in '{ $parent }' aanmaken created = '{ $name }' in '{ $parent }' aangemaakt copying = { $items } { $items -> [one] bestand *[other] bestanden } van '{ $from }' naar '{ $to }' kopiëren ({ $progress })... copied = { $items } { $items -> [one] bestand *[other] bestanden } gekopieerd van '{ $from }' naar '{ $to }' deleting = { $items } { $items -> [one] bestand *[other] bestanden } uit { trash } verwijderen ({ $progress })... deleted = { $items } { $items -> [one] bestand *[other] bestanden } verwijderd uit { trash } emptying-trash = { trash } recyclen ({ $progress })... emptied-trash = { trash } gerecycled extracting = { $items } { $items -> [one] bestand *[other] bestanden } van '{ $from }' naar '{ $to }' uitpakken ({ $progress })... extracted = { $items } { $items -> [one] bestand *[other] bestanden } uitgepakt van '{ $from }' naar '{ $to }' setting-executable-and-launching = '{ $name }' uitvoerbaar maken en starten set-executable-and-launched = '{ $name }' uitvoerbaar gemaakt en gestart setting-permissions = Rechten voor '{ $name }' wijzigen in '{ $mode }' set-permissions = Rechten voor '{ $name }' gewijzigd in '{ $mode }' moving = { $items } { $items -> [one] bestand *[other] bestanden } van '{ $from }' naar '{ $to }' verplaatsen ({ $progress })... moved = { $items } { $items -> [one] bestand *[other] bestanden } verplaatst van '{ $from }' naar '{ $to }' permanently-deleting = { $items } { $items -> [one] bestand *[other] bestanden } premanent verwijderen permanently-deleted = { $items } { $items -> [one] bestand *[other] bestanden } permanent verwijderd renaming = '{ $from }' als '{ $to }' hernoemen renamed = '{ $from }' als '{ $to }' hernoemd restoring = { $items } { $items -> [one] bestand *[other] bestanden } uit { trash } terugzetten ({ $progress })... restored = { $items } { $items -> [one] bestand *[other] bestanden } uit { trash } teruggezet unknown-folder = Onbekende map ## Open with menu-open-with = Openen met… default-app = { $name } (standaard) ## Show details show-details = Details weergeven type = Type: { $mime } items = Bestanden: { $items } item-size = Grootte: { $size } item-created = Aangemaakt op: { $created } item-modified = Bewerkt op: { $modified } item-accessed = Geopend op: { $accessed } calculating = Wordt berekend… ## Settings settings = Instellingen single-click = Een keer klikken om te openen ### Appearance appearance = Uiterlijk theme = Thema match-desktop = Systeemstandaard dark = Donker light = Licht ### Type to Search type-to-search = Typ om te zoeken type-to-search-recursive = In deze map en alle onderliggende mappen zoeken type-to-search-enter-path = Naar de bestandslocatie of -naam zoeken # Context menu add-to-sidebar = Favoriet aan zijbalk toevoegen compress = Comprimeren delete-permanently = Permanent verwijderen extract-here = Uitpakken new-file = Nieuw bestand… new-folder = Nieuwe map… open-in-terminal = In terminal openen move-to-trash = Naar prullenbak verplaatsen restore-from-trash = Uit prullenbak terugzetten remove-from-sidebar = Favoriet uit zijbalk verwijderen sort-by-name = Sorteren op naam sort-by-modified = Sorteren op laatst bewerkt sort-by-size = Sorteren op grootte sort-by-trashed = Sorteren op tijdstip van verwijderen ## Desktop change-wallpaper = Schermachtergrond wijzigen... desktop-appearance = Uiterlijk van het bureaublad… display-settings = Beeldschermbeheer... # Menu ## File file = Bestand new-tab = Nieuw tabblad new-window = Nieuw venster reload-folder = Opnieuw laden rename = Hernoemen… close-tab = Tabblad sluiten quit = Sluiten ## Edit edit = Bewerken cut = Knippen copy = Kopiëren paste = Plakken select-all = Alles selecteren ## View zoom-in = Inzoomen default-size = Standaardgrootte zoom-out = Uitzoomen view = Beeld grid-view = Rasterweergave list-view = Lijstweergave show-hidden-files = Verborgen bestanden tonen list-directories-first = Mappen bovenaan weergeven gallery-preview = Galerijweergave menu-settings = Instellingen… menu-about = Over COSMIC Bestanden… ## Sort sort = Sorteren sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Nieuwste bovenaan sort-oldest-first = Oudste bovenaan sort-smallest-to-largest = Van klein naar groot sort-largest-to-smallest = Van groot naar klein progress-failed = { $percent }%, mislukt eject = Uitwerpen support = Ondersteuning removing-from-recents = { $items } { $items -> [one] item *[other] items } van { recents } pasted-image = Geplakte afbeelding pasted-text = Geplakte tekst pasted-video = Geplakte video repository = Broncode empty-trash-title = Prullenbak leegmaken? removed-from-recents = { $items } { $items -> [one] item *[other] items } uit { recents } verwijderd remove-from-recents = Uit recente verwijderen type-to-search-select = Dit selecteert het eerst overeenkomende bestand of map comment = Bestandsbeheerder voor COSMIC desktop copy-to-title = Kopieerbestemming aanwijzen copy-to-button-label = Kopiëren move-to-button-label = Verplaatsen copy-to = Kopiëren naar… move-to = Verplaatsen naar… keywords = Bestand;Map;Document;Verkenner move-to-title = Verplaatsbestemming aanwijzen ================================================ FILE: i18n/nn/cosmic_files.ftl ================================================ cosmic-files = COSMIC Filer open-file = Opna fil empty-folder = Tom mappe cancel = Avbryt password = Passord settings = Innstillingar skip = Hopp over name = Namn save = Lagra delete = Slett ================================================ FILE: i18n/oc/cosmic_files.ftl ================================================ ================================================ FILE: i18n/pa/cosmic_files.ftl ================================================ empty-folder = ਖਾਲੀ ਫੋਲਡਰ empty-folder-hidden = ਖਾਲੀ ਫੋਲਡਰ (ਲੁਕਵੀਆਂ ਚੀਜ਼ਾਂ ਹਨ) no-results = ਕੋਈ ਨਤੀਜਾ ਨਹੀਂ ਲੱਭਿਆ filesystem = ਫ਼ਾਇਲ-ਸਿਸਟਮ home = ਮੁੱਖ ਸਫ਼ਾ networks = ਨੈੱਟਵਰਕ notification-in-progress = ਫ਼ਾਇਲ ਕਾਰਵਾਈਆਂ ਜਾਰੀ ਹਨ trash = ਰੱਦੀ recents = ਸੱਜਰੇ undo = ਵਾਪਸ today = ਅੱਜ desktop-view-options = ਡੈਸਕਟਾਪ ਝਲਕ ਲਈ ਚੋਣਾਂ... show-on-desktop = ਡੈਸਕਟਾਪ ਉਤੇ ਵੇਖਾਓ desktop-folder-content = ਡੈਸਕਟਾਪ ਫੋਲਡਰ ਸਮੱਗਰੀ mounted-drives = ਮਾਊਂਟ ਕੀਤੇ ਡਿਵਾਈਸ trash-folder-icon = ਰੱਦੀ ਫੋਲਡਰ ਦਾ ਆਈਕਾਨ icon-size-and-spacing = ਆਈਕਾਨ ਦਾ ਆਕਾਰ ਅਤੇ ਫ਼ਾਸਲਾ icon-size = ਆਈਕਾਨ ਦਾ ਆਕਾਰ grid-spacing = ਗਰਿੱਡ ਫ਼ਾਸਲਾ name = ਨਾਂ modified = ਸੋਧ ਕੀਤੀ trashed-on = ਰੱਦੀ 'ਚ ਭੇਜਿਆ size = ਆਕਾਰ details = ਵੇਰਵੇ dismiss = ਸੁਨੇਹੇ ਨੂੰ ਖ਼ਾਰਜ ਕਰੋ operations-running = { $running } { $running -> [one] ਕਾਰਵਾਈ *[other] ਕਾਰਵਾਈਆਂ } ਜਾਰੀ ਹਨ ({ $percent }%)... pause = ਵਿਰਾਮ resume = ਮੁੜ-ਚਾਲੂ create-archive = ਅਕਾਇਵ ਬਣਾਓ extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ extract-to = ਖਿਲਾਰੋ... extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ? empty-trash-warning = ਰੱਦੀ ਫੋਲਡਰ ਵਿਚਲੀਆਂ ਚੀਜ਼ਾਂ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾ ਦਿੱਤੀਆਂ ਜਾਣਗੀਆਂ create-new-file = ਨਵੀਂ ਫ਼ਾਇਲ ਬਣਾਓ create-new-folder = ਨਵਾਂ ਫੋਲਡਰ ਬਣਾਓ file-name = ਫ਼ਾਇਲ ਦਾ ਨਾਂ folder-name = ਫੋਲਡਰ ਦਾ ਨਾਂ file-already-exists = ਉਸ ਨਾਂ ਨਾਲ ਫ਼ਾਇਲ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ folder-already-exists = ਉਸ ਨਾਂ ਨਾਲ ਫੋਲਡਰ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ name-hidden = ਨਾਂ ਦੇ ਸ਼ੁਰੂ ਵਿੱਚ "." ਹੋਣ ਵਾਲੀ ਚੀਜ਼ ਲੁਕਵੀਂ ਹੋਵੇਗੀ name-invalid = ਨਾਂ "{ $filename }" ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ name-no-slashes = ਨਵਾਂ ਵਿੱਚ ਸਲੈਸ਼ ਨਹੀ ਹੋ ਸਕਦੀ ਹੈ cancel = ਰੱਦ ਕਰੋ create = ਬਣਾਓ open = ਖੋਲ੍ਹੋ open-file = ਫ਼ਾਇਲ ਨੂੰ ਖੋਲ੍ਹੋ open-folder = ਫੋਲਡਰ ਨੂੰ ਖੋਲ੍ਹੋ open-in-new-tab = ਨਵੀਂ ਟੈਬ ਵਿੱਚ ਖੋਲ੍ਹੋ open-in-new-window = ਨਵੀਂ ਵਿੰਡੋ ਵਿੱਚ ਖੋਲ੍ਹੋ cosmic-files = COSMIC ਫ਼ਾਇਲਾਂ operations-running-finished = { $running } { $running -> [one] ਕਾਰਵਾਈ *[other] ਕਾਰਵਾਈਆਂ } ਚੱਲ ਰਹੀ ਹੈ ({ $percent }%), { $finished } ਮੁਕੰਮਲ... mount-error = ਡਰਾਇਵ ਵਰਤਣ ਲਈ ਅਸਮਰੱਥ open-item-location = ਆਈਟਮ ਟਿਕਾਣੇ ਨੂੰ ਖੋਲ੍ਹੋ open-multiple-files = ਕਈ ਫ਼ਾਇਲਾਂ ਨੂੰ ਖੋਲ੍ਹੋ open-multiple-folders = ਕਈ ਫੋਲਡਰਾਂ ਨੂੰ ਖੋਲ੍ਹੋ save = ਸੰਭਾਲੋ save-file = ਫ਼ਾਇਲ ਨੂੰ ਸੰਭਾਲੋ open-with-title = ਤੁਸੀ "{ $name }" ਨੂੰ ਕਿਵੇਂ ਖੋਲ੍ਹਣਾ ਚਾਹੁੰਦੇ ਹੋ? browse-store = { $store } ਦੀ ਝਲਕ ਵੇਖੋ other-apps = ਹੋਰ ਐਪਲੀਕੇਸ਼ਨਾਂ related-apps = ਸੰਬੰਧਿਤ ਐਪਲੀਕੇਸ਼ਨਾਂ permanently-delete-question = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਉਣਾ ਹੈ? delete = ਹਟਾਓ rename-file = ਫ਼ਾਇਲ ਦਾ ਨਾਂ ਬਦਲੋ rename-folder = ਫੋਲਡਰ ਦਾ ਨਾਂ ਬਦਲੋ replace = ਬਦਲੋ original-file = ਅਸਲ ਫ਼ਾਇਲ replace-with = ਇਸ ਨਾਲ ਬਦਲੋ apply-to-all = ਸਭ ਉੱਤੇ ਲਾਗੂ ਕਰੋ keep-both = ਦੋਵਾਂ ਨੂੰ ਰੱਖੋ skip = ਛੱਡੋ set-and-launch = ਸੈੱਟ ਕਰੋ ਅਤੇ ਚਲਾਓ open-with = ਇਸ ਨਾਲ ਖੋਲ੍ਹੋ owner = ਮਾਲਕ group = ਗਰੁੱਪ other = ਹੋਰ none = ਕੋਈ ਨਹੀਂ execute-only = ਸਿਰਫ਼ ਚੱਲਣ write-only = ਸਿਰਫ਼ ਲਿਖਣ write-execute = ਸਿਖਣ ਅਤੇ ਚੱਲਣ read-only = ਸਿਰਫ਼ ਪੜ੍ਹਨ read-execute = ਪੜ੍ਹਨ ਅਤੇ ਚੱਲਣ read-write = ਪੜ੍ਹਨ ਅਤੇ ਲਿਖਣ read-write-execute = ਪੜ੍ਹਨ, ਲਿਖਣ ਅਤੇ ਚੱਲਣ remove = ਹਟਾਓ keep = ਰੱਖੋ repository = ਰਿਪੋਜ਼ਟਰੀ support = ਸਹਿਯੋਗ connect = ਕਨੈਕਟ ਕਰੋ remember-password = ਪਾਸਵਰਡ ਨੂੰ ਯਾਦ ਰੱਖੋ try-again = ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ username = ਵਰਤੋਂਕਾਰ ਦਾ ਨਾਂ cancelled = ਰੱਦ ਕੀਤਾ history = ਅਤੀਤ pending = ਬਕਾਇਆ progress = { $percent }% menu-open-with = Open with...ਨਾਲ ਖੋਲ੍ਹੋ default-app = { $name } (ਮੂਲ) show-details = ਵੇਰਵਿਆਂ ਨੂੰ ਵੇਖੋ type = ਕਿਸਮ: { $mime } selected-items = { $items } ਚੁਣੀਆਂ ਆਈਟਮਾਂ replace-title = "{ $filename }" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਮੌਜੂਦ ਹੈ favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ connecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ... domain = ਡੋਮੇਨ enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ password = ਪਾਸਵਰਡ edit-history = ਸੋਧ ਦਾ ਅਤੀਤ progress-cancelled = { $percent }%, ਰੱਦ ਕੀਤਾ progress-failed = { $percent }%, ਅਸਫ਼ਲ ਹੈ progress-paused = { $percent }%, ਵਿਰਾਮ ਹੈ failed = ਅਸਫ਼ਲ complete = ਪੂਰਾ emptying-trash = { trash } ਨੂੰ ਖਾਲੀ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ ({ $progress })... emptied-trash = { trash } ਨੂੰ ਖਾਲੀ ਕੀਤਾ settings = ਸੈਟਿੰਗਾਂ appearance = ਦਿੱਖ theme = ਥੀਮ new-file = ਨਵੀਂ ਫ਼ਾਇਲ... new-folder = ਨਵਾਂ ਫੋਲਡਰ... open-in-terminal = ਟਰਮੀਨਲ ਵਿੱਚ ਖੋਲ੍ਹੋ move-to-trash = ਰੱਦੀ ਵਿੱਚ ਭੇਜੋ restore-from-trash = ਰੱਦੀ ਵਿੱਚੋਂ ਬਹਾਲ ਕਰੋ sort-by-name = ਨਾਂ ਨਾਲ ਲੜੀਬੱਧ sort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ change-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ... desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ... display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ... file = ਫ਼ਾਇਲ new-tab = ਨਵੀਂ ਟੈਬ new-window = ਨਵੀਂ ਵਿੰਡੋ rename = ਨਾਂ ਨੂੰ ਬਦਲੋ... close-tab = ਟੈਬ ਨੂੰ ਬੰਦ ਕਰੋ quit = ਬਾਹਰ edit = ਸੋਧੋ cut = ਕੱਟੋ copy = ਕਾਪੀ ਕਰੋ paste = ਚੇਪੋ select-all = ਸਭ ਨੂੰ ਚੁਣੋ zoom-in = ਜ਼ੂਮ ਇਨ default-size = ਮੂਲ ਆਕਾਰ zoom-out = ਜ਼ੂਮ ਆਉਟ view = ਵੇਖੋ grid-view = ਗਰਿੱਡ ਝਲਕ show-hidden-files = ਲੁਕਵੀਆਂ ਫ਼ਾਇਲਾਂ ਨੂੰ ਵੇਖੋ menu-settings = ਸੈਟਿੰਗਾਂ... list-directories-first = ਡਾਇਰੈਕਟਰੀਆਂ ਨੂੰ ਪਹਿਲਾਂ ਵੇਖੋ sort = ਲੜੀਬੱਧ gallery-preview = ਗੈਲਰੀ ਝਲਕ sort-newest-first = ਨਵੀਆਂ ਪਹਿਲਾਂ sort-oldest-first = ਪੁਰਾਣੀਆਂ ਪਹਿਲਾਂ comment = COSMIC ਡੈਸਕਟਾਪ ਲਈ ਫ਼ਾਇਲ ਮੈਨੇਜਰ keywords = ਫੋਲਡਰ;ਮੈਨੇਜਰ; copy-to-title = ਕਾਪੀ ਕਰਨ ਲਈ ਟਿਕਾਣੇ ਨੂੁੰ ਚੁਣੋ copy-to-button-label = ਕਾਪੀ ਕਰੋ move-to-title = ਭੇਜਣ ਲਈ ਟਿਕਾਣੇ ਨੂੁੰ ਚੁਣੋ move-to-button-label = ਭੇਜੋ permanently-delete-warning = { $target } ਨੂੰ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਇਆ ਜਾਵੇਗਾ। ਇਹ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਲਿਆ ਜਾ ਸਕਦਾ ਹੈ। replace-warning-operation = ਕੀ ਤੁਸੀਂ ਇਸ ਨੂੰ ਬਦਲ ਦੇਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਨੂੰ ਬਦਲਣ ਨਾਲ ਸਮੱਗਰੀ ਉੱਤੇ ਲਿਖਿਆ ਜਾਵੇਗਾ। set-executable-and-launch = ਚੱਲਣਯੋਗ ਵਜੋਂ ਸੈੱਟ ਕਰੋ ਅਤੇ ਚਲਾਓ no-history = ਅਤੀਤ ਵਿੱਚ ਕੋਈ ਵੀ ਚੀਜ਼ ਨਹੀਂ ਹੈ। pasted-text = ਲਿਖਤ ਨੂੰ ਚੇਪਿਆ pasted-video = ਵੀਡੀਓ ਨੂੰ ਚੇਪਿਆ creating = "{ $parent }" ਵਿੱਚ "{ $name }" ਨੂੰ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ created = "{ $parent }" ਵਿੱਚ "{ $name }" ਨੂੰ ਬਣਾਇਆ ਗਿਆ renaming = "{ $from }" ਦਾ ਨਾਂ "{ $to }" ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ renamed = "{ $from }" ਦਾ ਨਾਂ ਬਦਲ ਕੇ "{ $to }" ਕੀਤਾ ਗਿਆ items = ਚੀਜ਼ਾਂ: { $items } item-size = ਆਕਾਰ: { $size } item-created = ਬਣਾਇਆ: { $created } item-modified = ਸੋਧ ਕੀਤੀ: { $modified } item-accessed = ਪਹੁੰਚ: { $accessed } calculating = ਗਿਣਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ... single-click = ਇੱਕ ਵਾਰ ਕਲਿੱਕ ਕਰਕੇ ਖੋਲ੍ਹੋ show-recents = ਬਾਹੀ ਵਿੱਚ ਸੱਜਰੇ ਫੋਲਡਰ match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਦਾ dark = ਗੂੜ੍ਹਾ light = ਹਲਕਾ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ compress = ਕੰਪਰੈਸ copy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ... delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ eject = ਬਾਹਰ extract-here = ਖਿਲਾਰੋ move-to = ਇੱਥੇ ਭੇਜੋ... remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ list-view = ਸੂਚੀ ਝਲਕ menu-about = COSMIC ਫਾਇਲਾਂ ਬਾਰੇ... sort-a-z = ਓ-ੜ sort-z-a = ੜ-ਓ sort-smallest-to-largest = ਛੋਟੇ ਤੋਂ ਵੱਡਾ sort-largest-to-smallest = ਵੱਡੇ ਤੋਂ ਛੋਟਾ ================================================ FILE: i18n/pl/cosmic_files.ftl ================================================ cosmic-files = Pliki COSMIC comment = Menedżer plików pulpitu COSMIC keywords = Katalogi;Pliki;Menedżer; empty-folder = Pusty katalog empty-folder-hidden = Pusty katalog (z ukrytymi plikami) no-results = Brak wyników filesystem = System plików home = Katalog domowy networks = Sieci notification-in-progress = Operacje na plikach w toku trash = Kosz recents = Poprzednie undo = Cofnij today = Dzisiaj # Desktop view options desktop-view-options = Opcje widoku pulpitu… show-on-desktop = Pokaż na Pulpicie desktop-folder-content = Zawartość katalogu Pulpit mounted-drives = Podpięte dyski trash-folder-icon = Ikona kosza icon-size-and-spacing = Rozmiar i rozstaw ikon icon-size = Rozmiar ikon grid-spacing = Rozstaw siatki # List view name = Nazwa modified = Zmodyfikowano trashed-on = Wyrzucono do kosza size = Rozmiar # Progress footer details = Detale dismiss = Odrzuć wiadomość operations-running = { $running } bieżące { $running -> [one] działanie *[other] działania } ({ $percent }%)… operations-running-finished = { $running } bieżące { $running -> [one] działanie *[other] działania } ({ $percent }%), { $finished } ukończone… pause = Wstrzymaj resume = Wznów # Dialogs ## Compress Dialog create-archive = Utwórz archiwum ## Extract Dialog extract-password-required = Wymagane hasło extract-to = Wypakuj do… extract-to-title = Wypakuj do katalogu ## Empty Trash Dialog empty-trash = Opróżnienie kosza empty-trash-warning = Elementy z kosza zostaną bezpowrotnie usunięte ## Mount Error Dialog mount-error = Brak dostępu do dysku # New File/Folder Dialog create-new-file = Utwórz nowy plik create-new-folder = Utwórz nowy katalog file-name = Nazwa pliku folder-name = Nazwa katalogu file-already-exists = Plik z taką nazwą już istnieje folder-already-exists = Katalog z taką nazwą już istnieje name-hidden = Nazwy zaczynające się od „.” będą ukryte name-invalid = Musisz zmienić nazwę na inną z „{ $filename }” name-no-slashes = Nazwa nie może zawierać ukośników # Open/Save Dialog cancel = Anuluj open = Otwórz create = Utwórz open-file = Otwórz plik open-folder = Otwórz katalog open-in-new-tab = Otwórz w nowej karcie open-in-new-window = Otwórz w nowym oknie open-item-location = Otwórz położenie elementu open-multiple-files = Otwórz wiele plików open-multiple-folders = Otwórz wiele katalogów save = Zapisz save-file = Zapisz plik ## Open With Dialog open-with-title = Czym chcesz otworzyć „{ $name }”? browse-store = Przeglądaj { $store } other-apps = Inne aplikacje related-apps = Pokrewne aplikacje ## Permanently delete Dialog selected-items = { $items } zaznaczonych elementów permanently-delete-question = Definitywnie usunąć? delete = Usuń permanently-delete-warning = { $target } będzie bezpowrotnie usunięte. Nie będzie można tego przywrócić. # Rename Dialog rename-file = Zmień nazwę pliku rename-folder = Zmień nazwę katalogu # Replace Dialog replace = Zastąp replace-title = „{ $filename }” już istnieje w tym miejscu replace-warning = Czy chcesz by został on zastąpiony przez wybrany element? To nadpisze jego zawartość. replace-warning-operation = Czy chcesz by został on zastąpiony? To nadpisze jego zawartość. original-file = Oryginalny plik replace-with = Zastąpiony przez apply-to-all = Zastosuj do wszystkich keep-both = Zachowaj oba skip = Pomiń ## Set as Executable and Launch Dialog set-executable-and-launch = Ustaw jako wykonywalny i uruchom set-executable-and-launch-description = Czy chcesz ustawić plik „{ $name }” jako wykonywalny i uruchomić go? set-and-launch = Ustaw i uruchom ## Metadata Dialog open-with = Otwórz za pomocą owner = Właściciel group = Grupa other = Inni ### Mode 0 none = Brak ### Mode 1 (unusual) execute-only = Tylko wykonywanie ### Mode 2 (unusual) write-only = Tylko zapis ### Mode 3 (unusual) write-execute = Zapis i wykonywanie ### Mode 4 read-only = Tylko odczyt ### Mode 5 read-execute = Odczyt i wykonywanie ### Mode 6 read-write = Odczyt i zapis ### Mode 7 read-write-execute = Odczyt, zapis i wykonywanie ## Favorite Path Error Dialog favorite-path-error = Błąd podczas otwierania katalogu favorite-path-error-description = Nie można otworzyć „{ $path }” Może nie istnieć lub możesz nie mieć uprawnień do jego otwierania Czy chcesz go usunąć z panelu bocznego? remove = Usuń keep = Zachowaj # Context Pages ## About repository = Repozytorium support = Wsparcie ## Add Network Drive add-network-drive = Dodaj dysk sieciowy connect = Połącz connect-anonymously = Połącz anonimowo connecting = Łączenie… domain = Domena enter-server-address = Wprowadź adres serwera network-drive-description = Adres serwera zawiera prefiks protokołu i adres. Przykładowo: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Dostępne protokoły,Prefiks AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDAV,dav:// or davs:// network-drive-error = Brak dostępu do dysku sieciowego password = Hasło remember-password = Zapamiętaj hasło try-again = Spróbuj ponownie username = Nazwa użytkownika ## Operations cancelled = Anulowano edit-history = Historia edycji history = Historia no-history = Brak pozycji w historii. pending = Oczekujące progress = { $percent }% progress-cancelled = { $percent }%, anulowano progress-failed = { $percent }%, nieudane progress-paused = { $percent }%, wstrzymano failed = Nieudane complete = Ukończone compressing = Spakuj { $items } { $items -> [one] element [few] elementy *[other] elementów } z „{ $from }” do „{ $to }” ({ $progress })… compressed = Spakowano { $items } { $items -> [one] element [few] elementy *[other] elementów } z „{ $from }” do „{ $to }” copy_noun = Kopiuj creating = Tworzy „{ $name }” w „{ $parent }” created = Stworzono „{ $name }” w „{ $parent }” copying = Kopiowanie { $items } { $items -> [one] elementu *[other] elementów } z „{ $from }” do „{ $to }” ({ $progress })… copied = Skopiowano { $items } { $items -> [one] element [few] elementy *[other] elementów } z „{ $from }” do „{ $to }” deleting = Usuwanie { $items } { $items -> [one] elementu *[other] elementów } z { trash } ({ $progress })... deleted = Usunięto { $items } { $items -> [one] element [few] elementy *[other] elementów } z { trash } emptying-trash = Opróżnianie { trash } ({ $progress })… emptied-trash = Opróżniono { trash } extracting = Wypakowywanie { $items } { $items -> [one] elementu *[other] elementów } z „{ $from }” do „{ $to }” ({ $progress })… extracted = Wypakowano { $items } { $items -> [one] element [few] elementy *[other] elementów } z „{ $from }” do „{ $to }” setting-executable-and-launching = Ustawianie „{ $name }” jako wykonywalnego i uruchamianie set-executable-and-launched = Ustaw „{ $name }” jako wykonywalny i uruchom setting-permissions = Ustawianie uprawnień dla „{ $name }” na { $mode } set-permissions = Ustaw uprawnienia dla „{ $name }” na { $mode } moving = Przenoszenie { $items } { $items -> [one] elementu *[other] elementów } z „{ $from }” do „{ $to }” ({ $progress })… moved = Przeniesiono { $items } { $items -> [one] element [few] elementy *[other] elementów } z „{ $from }” do „{ $to }” permanently-deleting = Definitywne usuwanie "{ $items }" "{ $items -> [one] elementu *[other] elementów }" permanently-deleted = Definitywnie usunięto "{ $items }" "{ $items -> [one] element [few] elementy *[other] elementów }" removing-from-recents = Usuwanie { $items } { $items -> [one] elementu *[other] elementów } z Poprzednich removed-from-recents = Usunięto { $items } { $items -> [one] element [few] elementy *[other] elementów } z Poprzednich renaming = Zmieniana nazwa z „{ $from }” na „{ $to }” renamed = Zmieniono nazwę z „{ $from }” na „{ $to }” restoring = Przywracanie { $items } { $items -> [one] elementu *[other] elementów } z Kosza ({ $progress })… restored = Przywrócono { $items } { $items -> [one] element [few] elementy *[other] elementów } z Kosza unknown-folder = nieznany katalog ## Open with menu-open-with = Otwórz za pomocą… default-app = { $name } (domyślnie) ## Show details show-details = Pokaż szczegóły type = Typ: { $mime } items = Elementy: { $items } item-size = Rozmiar: { $size } item-created = Utworzono: { $created } item-modified = Zmodyfikowano: { $modified } item-accessed = Otwarto: { $accessed } calculating = Obliczanie… ## Settings settings = Ustawienia single-click = Jedno kliknięcie by otwierać ### Appearance appearance = Wygląd theme = Motyw match-desktop = Dopasuj do Pulpitu dark = Ciemny light = Jasny ### Type to Search type-to-search = Zacznij pisać by wyszukać type-to-search-recursive = Wyszukuj w obecnym katalogu i jego podkatalogach type-to-search-enter-path = Wprowadź ścieżkę pliku lub katalogu # Context menu add-to-sidebar = Dodaj do bocznego panelu compress = Spakuj… delete-permanently = Usuń definitywnie eject = Wysuń extract-here = Wypakuj new-file = Nowy plik... new-folder = Nowy katalog... open-in-terminal = Otwórz w terminalu move-to-trash = Przenieś do kosza restore-from-trash = Przywróć z kosza remove-from-sidebar = Usuń z bocznego panelu sort-by-name = Uszereguj według nazwy sort-by-modified = Uszereguj według czasu modyfikacji sort-by-size = Uszereguj według rozmiaru sort-by-trashed = Uszereguj według czasu usunięcia remove-from-recents = Usuń z poprzednich ## Desktop change-wallpaper = Zmień tapetę… desktop-appearance = Wygląd pulpitu… display-settings = Ustawienia wyświetlacza… # Menu ## File file = Plik new-tab = Nowa karta new-window = Nowe okno reload-folder = Odśwież katalog rename = Zmień nazwę… close-tab = Zamknij kartę quit = Zamknij ## Edit edit = Edytuj cut = Wytnij copy = Kopiuj paste = Wklej select-all = Zaznacz wszystko ## View zoom-in = Zbliż default-size = Domyślny rozmiar zoom-out = Oddal view = Widok grid-view = Widok siatki list-view = Widok listy show-hidden-files = Pokaż ukryte pliki list-directories-first = Najpierw wyświetlaj katalogi gallery-preview = Podgąd galerii menu-settings = Ustawienia… menu-about = O Plikach COSMIC… ## Sort sort = Uszereguj sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Najpierw najnowsze sort-oldest-first = Najpierw najstarsze sort-smallest-to-largest = Najpierw najmniejsze sort-largest-to-smallest = Najpierw największe empty-trash-title = Opróżnić kosz? type-to-search-select = Wybierz pierwszy pasujący plik lub katalog pasted-image = Wklej Obraz pasted-text = Wklejony Tekst pasted-video = Wklejone Wideo copy-to-title = Wybierz gdzie skopiować copy-to-button-label = Kopiuj move-to-title = Wybierz gdzie przenieść move-to-button-label = Przenieś copy-to = Skopiuj do… 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 ================================================ FILE: i18n/pt/cosmic_files.ftl ================================================ cosmic-files = Ficheiros COSMIC comment = Gerenciador de arquivos do COSMIC keywords = Pastas;Gerenciador;Arquivos;Gestor;Explorer; empty-folder = Pasta vazia empty-folder-hidden = Pasta vazia (tem ficheiros ocultos) no-results = Nenhum resultado encontrado filesystem = Sistema de ficheiros home = Pasta Pessoal notification-in-progress = Operações em curso. trash = Lixo undo = Anular # List view name = Nome modified = Modificado size = Tamanho # Dialogs ## Empty Trash Dialog empty-trash = Esvaziar lixo empty-trash-warning = Pretende eliminar permanentemente todos os itens do Lixo? # New File/Folder Dialog create-new-file = Criar novo ficheiro create-new-folder = Criar nova pasta file-name = Nome do ficheiro folder-name = Nome da pasta file-already-exists = Já existe um ficheiro com esse nome. folder-already-exists = Já existe uma pasta com esse nome. name-hidden = Os nomes começados por "." serão ocultados. name-invalid = O nome não pode ser "{ $filename }". name-no-slashes = O nome não pode conter barras. # Open/Save Dialog cancel = Cancelar open = Abrir open-file = Abrir ficheiro open-folder = Abrir pasta open-in-new-tab = Abrir num novo separador open-in-new-window = Abrir numa nova janela open-item-location = Abrir localização do item open-multiple-files = Abrir vários ficheiros open-multiple-folders = Abrir várias pastas save = Guardar save-file = Guardar ficheiro # Rename Dialog rename-file = Renomear ficheiro rename-folder = Renomear pasta # Replace Dialog replace = Substituir replace-title = { $filename } já existe neste local. replace-warning = Substituí-lo pelo que está a guardar? Se o substituir, o seu conteúdo será substituído. replace-warning-operation = Pretende substituí-lo? Ao substituí-lo, o seu conteúdo será substituído. original-file = Ficheiro original replace-with = Substituir por apply-to-all = Aplicar a tudo keep-both = Manter ambos skip = Ignorar ## Metadata Dialog owner = Proprietário group = Grupo other = Outro # Context Pages ## About ## Operations edit-history = Editar histórico history = Histórico no-history = Nenhum item no histórico. pending = Pendentes failed = Com falha complete = Concluído copy_noun = Copiado creating = A criar "{ $name }" em "{ $parent }" created = "{ $name }" criado em "{ $parent }" copying = A copiar { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... copied = { $items } { $items -> [one] item copiado *[other] itens copiados } de "{ $from }" para "{ $to }" emptying-trash = A esvaziar { trash } ({ $progress })... emptied-trash = { trash } esvaziado extracting = A extrair { $items } { $items -> [one] item *[other] itens } de "{ $from }" par "{ $to }" ({ $progress })... extracted = { $items } { $items -> [one] item extraído *[other] itens extraídos } de "{ $from }" para "{ $to }" moving = A mover { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... moved = { $items } { $items -> [one] item movido *[other] itens movidos } de "{ $from }" para "{ $to }" renaming = A renomear "{ $from }" para "{ $to }" renamed = "{ $from }" renomeado para "{ $to }" restoring = A restaurar { $items } { $items -> [one] item *[other] itens } de { trash } ({ $progress })... restored = Restaurado { $items } { $items -> [one] item *[other] itens } para { trash } unknown-folder = pasta desconhecida ## Open with menu-open-with = Abrir com... default-app = { $name } (predefinição) ## Show details show-details = Mostrar detalhes ## Settings settings = Definições ### Appearance appearance = Aparência theme = Tema match-desktop = Estilo do sistema dark = Escuro light = Claro # Context menu extract-here = Extrair add-to-sidebar = Adicionar à barra lateral new-file = Novo ficheiro... new-folder = Nova pasta... open-in-terminal = Abrir no terminal move-to-trash = Mover para o lixo restore-from-trash = Restaurar do lixo remove-from-sidebar = Remover da barra lateral sort-by-name = Ordenar por nome sort-by-modified = Ordenar por data de modificação sort-by-size = Ordenar por tamanho # Menu ## File file = Ficheiro new-tab = Novo separador new-window = Nova janela rename = Renomear... close-tab = Fechar separador quit = Sair ## Edit edit = Editar cut = Cortar copy = Copiar paste = Colar select-all = Selecionar tudo ## View zoom-in = Aumentar default-size = Tamanho predefinido zoom-out = Diminuir view = Ver grid-view = Visualização em grelha list-view = Visualização em lista show-hidden-files = Mostrar ficheiros ocultos list-directories-first = Listar primeiro os diretórios menu-settings = Definições... menu-about = Acerca do Ficheiros COSMIC... repository = Repositório support = Suporte details = Detalhes dismiss = Dispensar mensagem remove = Remover cancelled = Canceladas networks = Redes recents = Recentes today = Hoje desktop-view-options = Opções de visualização da área de trabalho... show-on-desktop = Mostrar na área de trabalho desktop-folder-content = Conteúdo da pasta da área de trabalho mounted-drives = Dispositivos montados trash-folder-icon = Ícone do lixo icon-size-and-spacing = Tamanho e espaçamento do ícone icon-size = Tamanho do ícone grid-spacing = Espaçamento entre ícones trashed-on = Enviado para o lixo operations-running = { $running } { $running -> [one] operação *[other] operações } em execução ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operação *[other] operações } em execução ({ $percent }%), { $finished } concluídas... pause = Pausa resume = Retomar create-archive = Criar arquivo extract-password-required = Palavra-passe necessária extract-to = Extrair para... extract-to-title = Extrair para pasta mount-error = Não foi possível aceder ao dispositivo create = Criar open-with-title = Como pretende abrir "{ $name }"? browse-store = Procurar em { $store } other-apps = Outras aplicações related-apps = Aplicações relacionadas selected-items = os { $items } itens selecionados permanently-delete-question = Eliminar permanentemente delete = Eliminar permanently-delete-warning = Tem a certeza de que pretende eliminar { $target } permanentemente? Esta ação não pode ser anulada. set-executable-and-launch = Definir como executável e iniciar set-executable-and-launch-description = Pretende definir "{ $name }" como executável e iniciá-lo? set-and-launch = Definir e iniciar open-with = Abrir com none = Nenhum(a) execute-only = Executar-apenas write-only = Gravar-apenas write-execute = Gravação e execução read-only = Apenas-leitura read-execute = Leitura e execução read-write = Leitura e escrita read-write-execute = Leitura, gravação e execução favorite-path-error = Erro ao abrir diretório favorite-path-error-description = Não foi possível abrir "{ $path }". O item pode não existir ou não tem permissão para abri-lo. Pretende removê-lo da barra lateral? keep = Manter add-network-drive = Adicionar unidade de rede connect = Ligar connect-anonymously = Ligar anonimamente connecting = A ligar… domain = Domínio enter-server-address = Insira o endereço do servidor network-drive-description = Endereços de servidor incluem um prefixo de protocolo e um endereço. Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1] network-drive-schemes = Protocolos disponíveis,Prefixo AppleTalk,afp:// File Transfer Protocol,ftp:// ou ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// ou ssh:// WebDAV,dav:// ou davs:// network-drive-error = Não é possível aceder à unidade de rede password = Palavra-passe remember-password = Memorizar palavra-passe try-again = Tentar novamente username = Nome de utilizador progress = { $percent }% progress-cancelled = { $percent }%, cancelado progress-failed = { $percent }%, com falha progress-paused = { $percent }%, em pausa compressing = A comprimir { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... compressed = { $items } { $items -> [one] item comprimido *[other] itens comprimidos } de "{ $from }" para "{ $to }" deleting = A eliminar { $items } { $items -> [one] item *[other] itens } do { trash } ({ $progress })... deleted = { $items } { $items -> [one] item eliminado *[other] itens eliminados } do { trash } setting-executable-and-launching = A definir "{ $name }" como executável e a iniciar set-executable-and-launched = "{ $name }" definido como executável e iniciado setting-permissions = A definir permissões de "{ $name }" para { $mode } set-permissions = Definir permissões de "{ $name }" para { $mode } permanently-deleting = A eliminar permanentemente { $items } { $items -> [one] item *[other] itens } permanently-deleted = { $items } { $items -> [one] item eliminado *[other] itens eliminados } permanentemente removing-from-recents = A remover { $items } { $items -> [one] item *[other] itens } de { recents } removed-from-recents = { $items } { $items -> [one] item removido *[other] itens removidos } de { recents } type = Tipo: { $mime } items = Itens: { $items } item-size = Tamanho: { $size } item-created = Criado: { $created } item-modified = Modificado: { $modified } item-accessed = Acedido: { $accessed } calculating = A calcular... single-click = Um único clique para abrir type-to-search = Escreva para pesquisar type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas type-to-search-enter-path = Insere o caminho do diretório ou ficheiro compress = Comprimir delete-permanently = Eliminar permanentemente eject = Ejetar sort-by-trashed = Ordenar por data de eliminação remove-from-recents = Remover dos itens recentes change-wallpaper = Alterar papel de parede... desktop-appearance = Aparência da área de trabalho... display-settings = Definições do ecrã... reload-folder = Recarregar pasta gallery-preview = Pré-visualizar sort = Ordenar sort-a-z = A-Z sort-z-a = Z-A 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 ================================================ FILE: i18n/pt-BR/cosmic_files.ftl ================================================ cosmic-files = Gestor de Arquivos comment = Gerenciador de arquivos do ambiente COSMIC empty-folder = Pasta vazia empty-folder-hidden = Pasta vazia (contém itens ocultos) no-results = Nenhum item encontrado filesystem = Sistema de arquivos home = Pasta pessoal networks = Redes notification-in-progress = Há operações de arquivo em andamento trash = Lixeira recents = Recentes undo = Desfazer today = Hoje # Desktop view options desktop-view-options = Opções de visualização da área de trabalho... show-on-desktop = Mostrar na Área de trabalho desktop-folder-content = Conteúdo da pasta da área de trabalho mounted-drives = Dispositivos montados trash-folder-icon = Ícone da lixeira icon-size-and-spacing = Tamanho e espaçamento do ícone icon-size = Tamanho do ícone grid-spacing = Espaçamento entre ícones # List view name = Nome modified = Modificado trashed-on = Enviado à lixeira size = Tamanho # Progress footer details = Detalhes dismiss = Dispensar mensagem operations-running = { $running } { $running -> [one] operação *[other] operações } em andamento ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operação *[other] operações } em andamento ({ $percent }%), { $finished } concluídas... pause = Pausar resume = Continuar # Dialogs ## Compress Dialog create-archive = Compactar arquivos ## Extract Dialog extract-password-required = Senha necessária extract-to = Extrair para... extract-to-title = Extrair para pasta ## Empty Trash Dialog empty-trash = Esvaziar a lixeira empty-trash-warning = Todos os itens da Lixeira serão permanentemente excluídos ## Mount Error Dialog mount-error = Não é possível acessar a unidade ## New File/Folder Dialog create-new-file = Criar novo arquivo create-new-folder = Criar nova pasta file-name = Nome do arquivo folder-name = Nome da pasta file-already-exists = Já existe um arquivo com esse nome folder-already-exists = Já existe uma pasta com esse nome name-hidden = Os nomes que começam com "." serão ocultados name-invalid = O nome não pode ser "{ $filename }" name-no-slashes = O nome não pode conter barras ## Open/Save Dialog cancel = Cancelar create = Criar open = Abrir open-file = Abrir arquivo open-folder = Abrir pasta open-in-new-tab = Abrir em uma nova aba open-in-new-window = Abrir em uma nova janela open-item-location = Abrir local do item open-multiple-files = Abrir vários arquivos open-multiple-folders = Abrir várias pastas save = Salvar save-file = Salvar arquivo ## Open With Dialog open-with-title = Como deseja abrir "{ $name }"? browse-store = Procurar em { $store } other-apps = Outros aplicativos related-apps = Aplicativos relacionados ## Permanently delete Dialog selected-items = Os { $items } itens selecionados permanently-delete-question = Excluir permanentemente? delete = Excluir permanently-delete-warning = Deseja realmente excluir permanentemente { $target }? Esta operação não poderá ser desfeita. ## Rename Dialog rename-file = Renomear arquivo rename-folder = Renomear pasta ## Replace Dialog replace = Substituir replace-title = "{ $filename }" já existe neste local replace-warning = Deseja substituí-lo por aquele que está salvando? Ao substituí-lo, seu conteúdo será sobrescrito. replace-warning-operation = Deseja substituí-lo? Ao substituí-lo, seu conteúdo será sobrescrito. original-file = Arquivo original replace-with = Substituir por apply-to-all = Aplicar a todos keep-both = Manter ambos skip = Ignorar ## Set as Executable and Launch Dialog set-executable-and-launch = Definir como executável e iniciar set-executable-and-launch-description = Deseja definir "{ $name }" como executável e iniciá-lo? set-and-launch = Marcar e iniciar ## Metadata Dialog open-with = Abrir com owner = Proprietário group = Grupo other = Outros ### Mode 0 none = Nenhum ### Mode 1 (unusual) execute-only = Somente execução ### Mode 2 (unusual) write-only = Somente escrita ### Mode 3 (unusual) write-execute = Escrita e execução ### Mode 4 read-only = Somente leitura ### Mode 5 read-execute = Leitura e execução ### Mode 6 read-write = Leitura e escrita ### Mode 7 read-write-execute = Leitura, escrita e execução ## Favorite Path Error Dialog favorite-path-error = Erro ao abrir diretório favorite-path-error-description = Não foi possível abrir "{ $path }" "{ $path }" pode não existir ou você não tem permissão para abri-lo Deseja removê-lo da barra lateral? remove = Remover keep = Manter # Context Pages ## About repository = Repositório support = Suporte ## Add Network Drive add-network-drive = Adicionar local de rede connect = Conectar connect-anonymously = Conectar anonimamente connecting = Conectando... domain = Domínio enter-server-address = Insira o endereço do servidor network-drive-description = Endereços de servidor incluem um prefixo de protocolo e um endereço. Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1] ### Certifique-se de manter a vírgula que separa as colunas network-drive-schemes = Protocolos disponíveis,Prefixo AppleTalk,afp:// File Transfer Protocol,ftp:// ou ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// ou ssh:// WebDAV,dav:// ou davs:// network-drive-error = Não foi possível acessar o local de rede password = Senha remember-password = Lembrar senha try-again = Tente novamente username = Usuário ## Operations cancelled = Cancelado edit-history = Editar histórico history = Histórico no-history = Nenhum item no histórico. pending = Pendente progress = { $percent }% progress-cancelled = { $percent }%, cancelado progress-failed = { $percent }%, com falha progress-paused = { $percent }%, pausado failed = Com falha complete = Concluído compressing = Compactando { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... compressed = { $items } { $items -> [one] item compactado *[other] itens compactados } de "{ $from }" para "{ $to }" copy_noun = Copiar creating = Criando "{ $name }" em "{ $parent }" created = "{ $name }" criado em "{ $parent }" copying = Copiando { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... copied = { $items } { $items -> [one] item copiado *[other] itens copiados } de "{ $from }" para "{ $to }" deleting = Excluindo { $items } { $items -> [one] item *[other] itens } da { trash } ({ $progress })... deleted = { $items } { $items -> [one] item excluído *[other] itens excluídos } da { trash } emptying-trash = Esvaziando a { trash } ({ $progress })... emptied-trash = { trash } esvaziada extracting = Extraindo { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... extracted = { $items } { $items -> [one] item extraído *[other] itens extraídos } de "{ $from }" para "{ $to }" setting-executable-and-launching = Marcando "{ $name }" como executável e iniciando set-executable-and-launched = "{ $name }" marcado como executável e iniciado setting-permissions = Definindo permissões de "{ $name }" para { $mode } set-permissions = Definir permissões de "{ $name }" para { $mode } moving = Movendo { $items } { $items -> [one] item *[other] itens } de "{ $from }" para "{ $to }" ({ $progress })... moved = { $items } { $items -> [one] item movido *[other] itens movidos } de "{ $from }" para "{ $to }" permanently-deleting = Excluindo permanentemente { $items } { $items -> [one] item *[other] itens } permanently-deleted = { $items } { $items -> [one] item excluído *[other] itens excluídos } permanentemente removing-from-recents = Removendo { $items } { $items -> [one] item *[other] itens } de { recents } removed-from-recents = { $items } { $items -> [one] item removido *[other] itens removidos } de { recents } renaming = Renomeando "{ $from }" para "{ $to }" renamed = "{ $from }" renomeado para "{ $to }" restoring = Restaurando { $items } { $items -> [one] item *[other] itens } da { trash } ({ $progress })... restored = { $items } { $items -> [one] item *[other] itens } da { trash } restaurado(s) unknown-folder = pasta desconhecida ## Open with menu-open-with = Abrir com... default-app = { $name } (padrão) ## Show details show-details = Mostrar detalhes type = Tipo: { $mime } items = Itens: { $items } item-size = Tamanho: { $size } item-created = Criado: { $created } item-modified = Modificado: { $modified } item-accessed = Acessado: { $accessed } calculating = Calculando... ## Settings settings = Configurações single-click = Clique simples para abrir ### Appearance appearance = Aparência theme = Tema match-desktop = Estilo do sistema dark = Estilo escuro light = Estilo claro ### Type to Search type-to-search = Digite para pesquisar type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas type-to-search-enter-path = Insere o caminho do diretório ou arquivo # Context menu add-to-sidebar = Adicionar à barra lateral compress = Compactar... delete-permanently = Excluir permanentemente eject = Desmontar extract-here = Extrair new-file = Novo arquivo... new-folder = Nova pasta... open-in-terminal = Abrir no terminal move-to-trash = Mover para a lixeira restore-from-trash = Restaurar da lixeira remove-from-sidebar = Remover da barra lateral sort-by-name = Ordenar por nome sort-by-modified = Ordenar por data de modificação sort-by-size = Ordenar por tamanho sort-by-trashed = Ordernar por data de exclusão remove-from-recents = Remover dos itens recentes ## Desktop change-wallpaper = Alterar o plano de fundo... desktop-appearance = Aparência da área de trabalho... display-settings = Configurações da tela... # Menu ## File file = Arquivo new-tab = Nova aba new-window = Nova janela reload-folder = Recarregar pasta rename = Renomear... close-tab = Fechar aba quit = Sair ## Edit edit = Editar cut = Recortar copy = Copiar paste = Colar select-all = Selecionar tudo ## View zoom-in = Ampliar default-size = Tamanho padrão zoom-out = Reduzir view = Exibir grid-view = Exibição em grade list-view = Exibição em lista show-hidden-files = Mostrar arquivos ocultos list-directories-first = Listar pastas primeiro gallery-preview = Pré-visualizar menu-settings = Configurações... menu-about = Sobre o Gestor de Arquivos... ## Sort sort = Ordenar sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Mais novos 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 empty-trash-title = Esvaziar a lixeira? type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente pasted-image = Imagem colada pasted-text = Texto colado pasted-video = Vídeo colado copy-to-title = Selecione o destino da cópia copy-to-button-label = Copiar move-to-title = Selecione o destino da movimentação move-to-button-label = Mover copy-to = Copiar para... move-to = Mover para... keywords = Pasta;Gerenciador; show-recents = Pasta de recentes na barra lateral clear-recents-history = Limpar histórico de recentes copy-path = Copiar caminho mixed = Misto ================================================ FILE: i18n/ro/cosmic_files.ftl ================================================ cosmic-files = Fișiere COSMIC empty-folder = Dosar gol empty-folder-hidden = Dosar gol (conține elemente ascunse) no-results = Niciun rezultat găsit filesystem = Sistem de fișiere home = Acasă networks = Rețele notification-in-progress = Operațiuni de fișiere în desfășurare. trash = Coș de gunoi recents = Recente undo = Anulează today = Astăzi # Desktop view options desktop-view-options = Opțiuni de vizualizare desktop... show-on-desktop = Afișează pe desktop desktop-folder-content = Conținut dosar desktop mounted-drives = Unități montate trash-folder-icon = Pictogramă coș de gunoi icon-size-and-spacing = Dimensiune și spațiere pictograme icon-size = Dimensiune pictogramă grid-spacing = Spațiere grilă # List view name = Nume modified = Modificat trashed-on = Șters size = Dimensiune # Progress footer details = Detalii dismiss = Închide mesajul operations-running = { $running } operațiuni în desfășurare ({ $percent }%)... operations-running-finished = { $running } operațiuni în desfășurare ({ $percent }%), { $finished } finalizate... pause = Pauză resume = Reia # Dialogs ## Compress Dialog create-archive = Creează o arhivă ## Extract Dialog extract-password-required = Parolă necesară extract-to = Extrage în... extract-to-title = Extrage în dosar ## Empty Trash Dialog empty-trash = Golește coșul empty-trash-warning = Sigur dorești să ștergi definitiv toate elementele din coș? ## Dialog Eroare Montare mount-error = Nu se poate accesa unitatea ## New File/Folder Dialog create-new-file = Creează un fișier nou create-new-folder = Creează un dosar nou file-name = Nume fișier folder-name = Nume dosar file-already-exists = Un fișier cu acest nume există deja. folder-already-exists = Un dosar cu acest nume există deja. name-hidden = Numele care încep cu „.” vor fi ascunse. name-invalid = Numele nu poate fi "{ $filename }". name-no-slashes = Numele nu poate conține caractere „/”. ## Open/Save Dialog cancel = Anulează create = Creează open = Deschide open-file = Deschide fișier open-folder = Deschide dosar open-in-new-tab = Deschide în filă nouă open-in-new-window = Deschide în fereastră nouă open-item-location = Deschide locația elementului open-multiple-files = Deschide fișiere multiple open-multiple-folders = Deschide dosare multiple save = Salvează save-file = Salvează fișier ## Open With Dialog open-with-title = Cum dorești să deschizi „{ $name }”? browse-store = Răsfoiește în { $store } ## Rename Dialog rename-file = Redenumește fișier rename-folder = Redenumește dosar ## Replace Dialog replace = Înlocuiește replace-title = „{ $filename }” există deja în această locație. replace-warning = Dorești să-l înlocuiești cu cel pe care îl salvezi? Această acțiune va suprascrie conținutul. replace-warning-operation = Dorești să-l înlocuiești? Această acțiune va suprascrie conținutul. original-file = Fișier original replace-with = Înlocuiește cu apply-to-all = Aplică la toate keep-both = Păstrează ambele skip = Omitere ## Set as Executable and Launch Dialog set-executable-and-launch = Fă executabil și rulează set-executable-and-launch-description = Dorești să setezi „{ $name }” ca executabil și să îl rulezi? set-and-launch = Setează și rulează ## Metadata Dialog open-with = Deschide cu owner = Proprietar group = Grup other = Altele ### Mode 0 none = Niciuna ### Mode 1 (unusual) execute-only = Doar executare ### Mode 2 (unusual) write-only = Doar scriere ### Mode 3 (unusual) write-execute = Scriere și executare ### Mode 4 read-only = Doar citire ### Mode 5 read-execute = Citire și executare ### Mode 6 read-write = Citire și scriere ### Mode 7 read-write-execute = Citire, scriere și executare ## Favorite Path Error Dialog favorite-path-error = Eroare la deschiderea directorului favorite-path-error-description = Nu s-a putut deschide „{ $path }”. Este posibil să nu existe sau să nu ai permisiuni de acces. Vrei să-l elimini din bara laterală? remove = Elimină keep = Păstrează # Context Pages ## About ## Add Network Drive add-network-drive = Adaugă o unitate de rețea connect = Conectează connect-anonymously = Conectare anonimă connecting = Se conectează... domain = Domeniu enter-server-address = Introdu adresa serverului network-drive-description = Adresele serverului includ prefixul protocolului și adresa. Exemple: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Protocoale disponibile,Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// sau ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// sau ssh:// WebDav,dav:// sau davs:// network-drive-error = Nu se poate accesa unitatea de rețea password = Parolă remember-password = Ține minte parola try-again = Încearcă din nou username = Nume utilizator ## Operations cancelled = Anulat edit-history = Editează istoricul history = Istoric no-history = Nicio intrare în istoric. pending = În așteptare progress = { $percent }% progress-cancelled = { $percent }%, anulat progress-paused = { $percent }%, întrerupt failed = Eșuat complete = Complet compressing = Se comprimă { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” ({ $progress })... compressed = Comprimat { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” copy_noun = Copiere creating = Se creează „{ $name }” în „{ $parent }” created = S-a creat „{ $name }” în „{ $parent }” copying = Se copiază { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” ({ $progress })... copied = Copiat { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” deleting = Se șterge { $items } { $items -> [one] element *[other] elemente } din { trash } ({ $progress })... deleted = Șters { $items } { $items -> [one] element *[other] elemente } din { trash } emptying-trash = Se golește { trash } ({ $progress })... emptied-trash = Coșul { trash } a fost golit extracting = Se extrage { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” ({ $progress })... extracted = Extras { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” setting-executable-and-launching = Se setează „{ $name }” ca executabil și se rulează set-executable-and-launched = „{ $name }” setat ca executabil și rulat moving = Se mută { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” ({ $progress })... moved = Mutat { $items } { $items -> [one] element *[other] elemente } din „{ $from }” în „{ $to }” renaming = Se redenumește „{ $from }” în „{ $to }” renamed = S-a redenumit „{ $from }” în „{ $to }” restoring = Se restabilește { $items } { $items -> [one] element *[other] elemente } din { trash } ({ $progress })... restored = Restabilit { $items } { $items -> [one] element *[other] elemente } din { trash } unknown-folder = dosar necunoscut ## Open with menu-open-with = Deschide cu... default-app = { $name } (implicit) ## Show details show-details = Afișează detalii type = Tip: { $mime } items = Elemente: { $items } item-size = Dimensiune: { $size } item-created = Creat: { $created } item-modified = Modificat: { $modified } item-accessed = Accesat: { $accessed } calculating = Se calculează... ## Settings settings = Setări single-click = Un singur clic pentru deschidere ### Appearance appearance = Aspect theme = Temă match-desktop = Potrivește cu desktopul dark = Întunecat light = Deschis ### Type to Search type-to-search = Tastează pentru a căuta type-to-search-recursive = Caută în dosarul curent și subdosare type-to-search-enter-path = Introduce calea către dosar sau fișier # Context menu add-to-sidebar = Adaugă în bara laterală compress = Comprimă delete-permanently = Șterge definitiv extract-here = Extrage aici new-file = Fișier nou... new-folder = Dosar nou... open-in-terminal = Deschide în terminal move-to-trash = Mută în coș restore-from-trash = Recuperează din coș remove-from-sidebar = Elimină din bara laterală sort-by-name = Sortează după nume sort-by-modified = Sortează după modificare sort-by-size = Sortează după dimensiune sort-by-trashed = Sortează după dată ștergere ## Desktop change-wallpaper = Schimbă fundalul... desktop-appearance = Aspect desktop... display-settings = Setări ecran... # Menu ## File file = Fișier new-tab = Filă nouă new-window = Fereastră nouă rename = Redenumește... close-tab = Închide fila quit = Închide aplicația ## Edit edit = Editare cut = Taie copy = Copiază paste = Lipește select-all = Selectează tot ## View zoom-in = Mărește default-size = Dimensiune implicită zoom-out = Micșorează view = Vizualizare grid-view = Vizualizare grilă list-view = Vizualizare listă show-hidden-files = Afișează fișiere ascunse list-directories-first = Listează directoarele primele gallery-preview = Previzualizare galerie menu-settings = Setări... menu-about = Despre Fișierele COSMIC... ## Sort sort = Sortare sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Cele mai noi primele sort-oldest-first = Cele mai vechi primele sort-smallest-to-largest = De la mic la mare sort-largest-to-smallest = De la mare la mic ================================================ FILE: i18n/ru/cosmic_files.ftl ================================================ cosmic-files = Файлы empty-folder = Папка пуста empty-folder-hidden = Папка пуста (есть скрытые элементы) no-results = Ничего не найдено filesystem = Файловая система home = Домашняя папка trash = Корзина networks = Сеть notification-in-progress = Выполняются файловые операции recents = Недавние документы undo = Отменить today = Сегодня # List view name = Имя modified = Изменено trashed-on = Удалено size = Размер # Dialogs ## Compress Dialog create-archive = Создать архив ## Empty Trash Dialog empty-trash = Очистить корзину empty-trash-warning = Элементы в папке «Корзина» будут удалены без возможности восстановления # New File/Folder Dialog create-new-file = Создать новый файл create-new-folder = Создать новую папку file-name = Имя файла folder-name = Имя папки file-already-exists = Файл с таким именем уже существует. folder-already-exists = Папка с таким именем уже существует. name-hidden = Имена, начинающиеся на «.», будут скрыты. name-invalid = Имя не может быть «{ $filename }». name-no-slashes = Имя не должно содержать «/». # Open/Save Dialog cancel = Отмена open = Открыть open-file = Открыть файл open-folder = Открыть папку open-in-new-tab = Открыть в новой вкладке open-in-new-window = Открыть в новом окне open-item-location = Открыть расположение элемента open-multiple-files = Открыть несколько файлов open-multiple-folders = Открыть несколько папок save = Сохранить save-file = Сохранить файл # Rename Dialog rename-file = Переименовать файл rename-folder = Переименовать папку # Replace Dialog replace = Заменить replace-title = { $filename } уже существует в данном каталоге. replace-warning = Вы хотите заменить этот файл на тот, что сохраняете? Замена перезапишет все данные файла. replace-warning-operation = Хотите заменить? Замена приведёт к перезаписи содержимого. original-file = Оригинальный файл replace-with = Заменить на apply-to-all = Применить ко всем keep-both = Сохранить оба skip = Пропустить ## Metadata Dialog owner = Владелец group = Группа other = Прочие # Context Pages ## About ## Add Network Drive add-network-drive = Добавить сетевой диск connect = Подключиться connect-anonymously = Подключиться анонимно connecting = Подключение… domain = Домен enter-server-address = Введите адрес сервера network-drive-description = Адреса серверов включают префикс протокола и адрес. Пример: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns 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:// network-drive-error = Невозможно получить доступ к сетевому диску password = Пароль remember-password = Запомнить пароль try-again = Попробовать ещё раз username = Имя пользователя ## Operations edit-history = История редактирования history = История no-history = В истории нет записей. pending = В процессе failed = Не удалась complete = Завершена compressing = Сжатие { $items } { $items -> [one] элемента *[other] элем. } из «{ $from }» в «{ $to }» ({ $progress })… compressed = Сжато { $items } { $items -> [one] элемент *[other] элем. } из «{ $from }» в «{ $to }» copy_noun = Копирование creating = Создание { $name } в { $parent } created = Создан { $name } в { $parent } copying = Копирование { $items } { $items -> [one] элемента *[other] элем. } из «{ $from }» в «{ $to }» ({ $progress })… copied = Скопировано { $items } { $items -> [one] элемент *[other] элем. } из «{ $from }» в «{ $to }» emptying-trash = Очистка { trash } ({ $progress })… emptied-trash = { trash } очищена extracting = Извлечение { $items } { $items -> [one] элемента *[other] элем. } из «{ $from }» в «{ $to }» ({ $progress })… extracted = Извлечено { $items } { $items -> [one] элемент *[other] элем. } из «{ $from }» в «{ $to }» moving = Перемещение { $items } { $items -> [one] элемента *[other] элем. } из «{ $from }» в «{ $to }» ({ $progress })… moved = Перемещено { $items } { $items -> [one] элемент *[other] элем. } из «{ $from }» в «{ $to }» renaming = Переименование «{ $from }» в «{ $to }» renamed = «{ $from }» переименован в «{ $to }» restoring = Восстановление { $items } { $items -> [one] элемента *[other] элем. } из { trash } ({ $progress })… restored = Восстановлено { $items } { $items -> [one] элемент *[other] элем. } из { trash } unknown-folder = неизвестная папка ## Open with menu-open-with = Открыть с помощью… default-app = { $name } (по умолчанию) ## Show details show-details = Показать подробности ## Properties ## Settings settings = Параметры ### Appearance appearance = Оформление theme = Тема match-desktop = Как в системе dark = Тёмная light = Светлая # Context menu add-to-sidebar = Добавить на боковую панель compress = Сжать... extract-here = Распаковать new-file = Новый файл… new-folder = Новая папка… open-in-terminal = Открыть в терминале move-to-trash = Переместить в корзину restore-from-trash = Восстановить из корзины remove-from-sidebar = Убрать с боковой панели sort-by-name = Сорт. по имени sort-by-modified = Сорт. по дате изменения sort-by-size = Сорт. по размеру sort-by-trashed = Сорт. по дате удаления # Menu ## File file = Файл new-tab = Новая вкладка new-window = Новое окно rename = Переименовать… close-tab = Закрыть вкладку quit = Выйти ## Edit edit = Правка cut = Вырезать copy = Копировать paste = Вставить select-all = Выбрать все ## View zoom-in = Увеличить default-size = Размер по умолчанию zoom-out = Уменьшить view = Вид grid-view = Сетка list-view = Список show-hidden-files = Показывать скрытые файлы list-directories-first = Показывать сначала папки menu-settings = Параметры... menu-about = О приложении Файлы COSMIC... ## Sort sort = Сортировка sort-a-z = От А до Я sort-z-a = От Я до А sort-newest-first = Сначала новые sort-oldest-first = Сначала старые sort-smallest-to-largest = От меньшего к большему sort-largest-to-smallest = От большего к меньшему support = Поддержка repository = Репозиторий cancelled = Отменена details = Сведения dismiss = Скрыть сообщение remove = Убрать desktop-view-options = Параметры вида рабочего стола… show-on-desktop = Показывать на рабочем столе desktop-folder-content = Содержимое папки рабочего стола mounted-drives = Подключённые диски trash-folder-icon = Значок папки корзины icon-size-and-spacing = Размер и отступы значков icon-size = Размер значка grid-spacing = Отступ по сетке pause = Приостановить resume = Продолжить extract-password-required = Требуется пароль extract-to = Распаковать в… extract-to-title = Распаковать в папку mount-error = Не удалось получить доступ к диску create = Создать open-with-title = Как вы хотите открыть «{ $name }»? browse-store = Искать в { $store } other-apps = Другие приложения related-apps = Связанные приложения selected-items = { $items } выделенных элем. permanently-delete-question = Навсегда удалить delete = Удалить permanently-delete-warning = Вы уверены, что хотите навсегда удалить { $target }? Это действие необратимо. set-executable-and-launch = Сделать исполняемым и запустить set-executable-and-launch-description = Вы хотите сделать «{ $name }» исполняемым и запустить его? set-and-launch = Сделать и запустить open-with = Открывать в none = Нет прав execute-only = Только исполнение write-only = Только запись write-execute = Запись и исполнение read-only = Только чтение read-execute = Чтение и исполнение read-write = Чтение и запись read-write-execute = Чтение, запись, исполнение favorite-path-error = Не удалось открыть каталог favorite-path-error-description = Не удалось открыть «{ $path }». Возможно, он не существует, либо у вас нет прав на его открытие. Хотите убрать его с боковой панели? keep = Оставить progress = { $percent } % progress-cancelled = { $percent } %, отменена progress-failed = { $percent } %, не удалась progress-paused = { $percent } %, приостановлена setting-executable-and-launching = Установка «{ $name }» исполняемым и запуск set-executable-and-launched = «{ $name }» сделан исполнямым и запущен setting-permissions = Изменение прав доступа «{ $name }» на { $mode } set-permissions = Права доступа «{ $name }» изменены на { $mode } type = Тип: { $mime } items = Элементов: { $items } item-size = Размер: { $size } item-created = Дата создания: { $created } item-modified = Дата изменения: { $modified } item-accessed = Дата доступа: { $accessed } 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 = Галерея предпросмотра operations-running = { $running } { $running -> [one] операция *[other] опер. } выполняется ({ $percent } %)… operations-running-finished = { $running } { $running -> [one] операция *[other] опер. } выполняется ({ $percent } %), { $finished } завершено… deleting = Удаление { $items } { $items -> [one] элемента *[other] элем. } из { trash } ({ $progress })… deleted = Удалено { $items } { $items -> [one] элемент *[other] элем. } из { trash } permanently-deleting = Удаление навсегда { $items } { $items -> [one] элемента *[other] элем. } permanently-deleted = Удалено навсегда { $items } { $items -> [one] эелмент *[other] элем. } removing-from-recents = Убирание { $items } { $items -> [one] элемента *[other] элем. } из { recents } removed-from-recents = Убрано { $items } { $items -> [one] элемент *[other] элем. } из { recents } type-to-search-select = Выделение первого подходящего файла или папки pasted-image = Вставленное изображение pasted-text = Вставленный текст pasted-video = Вставленное видео copy-to-title = Выберите папку назначения copy-to-button-label = Копировать move-to-title = Выберите папку назначения move-to-button-label = Переместить copy-to = Копировать в... move-to = Переместить в... comment = Файловый менеджер для среды COSMIC keywords = Папка;Менеджер; show-recents = «Недавние документы» в бок. панели clear-recents-history = Очистить историю недавних copy-path = Копировать путь mixed = Смешанные ================================================ FILE: i18n/sk/cosmic_files.ftl ================================================ cosmic-files = Súbory COSMIC comment = Správca súborov pre prostredie COSMIC keywords = Priečinok;Správca;Súbory;Manažér;Prehliadač; empty-folder = Priečinok je prázdny empty-folder-hidden = Priečinok je prázdny (obsahuje skryté položky) no-results = Neboli nájdené žiadne výsledky filesystem = Súborový systém home = Domov networks = Siete notification-in-progress = Prebiehajú operácie so súbormi. trash = Kôš recents = Nedávne undo = Späť today = Dnes # Desktop view options desktop-view-options = Možnosti zobrazenia pracovnej plochy... show-on-desktop = Zobraziť na pracovnej ploche desktop-folder-content = Obsah priečinka Pracovná plocha mounted-drives = Pripojené disky trash-folder-icon = Ikona priečinka Kôš icon-size-and-spacing = Veľkosť ikon a rozostupy icon-size = Veľkosť ikon grid-spacing = Rozostupy mriežky # List view name = Názov modified = Upravené trashed-on = Vymazané size = Veľkosť # Progress footer details = Podrobnosti dismiss = Zavrieť správu operations-running = { $running } { $running -> [one] operácia [few] operácie [many] operácií *[other] operácie } prebieha ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] operácia [few] operácie [many] operácií *[other] operácie } prebieha ({ $percent }%), { $finished } dokončených... pause = Pozastaviť resume = Pokračovať # Dialogs ## Compress Dialog create-archive = Vytvoriť archív ## Extract Dialog extract-password-required = Vyžaduje sa heslo extract-to = Extrahovať do... extract-to-title = Extrahovať do priečinka ## Empty Trash Dialog empty-trash = Vyprázdniť kôš empty-trash-warning = Naozaj chcete trvalo odstrániť všetky položky v koši? ## Mount Error Dialog mount-error = Nie je možné získať prístup k disku ## New File/Folder Dialog create-new-file = Vytvoriť nový súbor create-new-folder = Vytvoriť nový priečinok file-name = Názov súboru folder-name = Názov priečinka file-already-exists = Súbor s týmto názvom už existuje. folder-already-exists = Priečinok s týmto názvom už existuje. name-hidden = Názvy začínajúce bodkou budú skryté. name-invalid = Názov nemôže byť "{ $filename }". name-no-slashes = Názov nemôže obsahovať lomítka. ## Open/Save Dialog cancel = Zrušiť create = Vytvoriť open = Otvoriť open-file = Otvoriť súbor open-folder = Otvoriť priečinok open-in-new-tab = Otvoriť v novej záložke open-in-new-window = Otvoriť v novom okne open-item-location = Otvoriť umiestnenie položky open-multiple-files = Otvoriť viac súborov open-multiple-folders = Otvoriť viac priečinkov save = Uložiť save-file = Uložiť súbor ## Open With Dialog open-with-title = Ako chcete otvoriť "{ $name }"? browse-store = Prehľadávať { $store } other-apps = Iné aplikácie related-apps = Súvisiace aplikácie ## Permanently delete Dialog selected-items = { $items } vybraných položiek permanently-delete-question = Trvalo odstrániť delete = Odstrániť permanently-delete-warning = Naozaj chcete trvalo odstrániť { $target }? Toto nie je možné vrátiť späť. ## Rename Dialog rename-file = Premenovať súbor rename-folder = Premenovať priečinok ## Replace Dialog replace = Nahradiť replace-title = "{ $filename }" už existuje v tomto umiestnení. replace-warning = Chcete ho nahradiť tým, ktorý práve ukladáte? Nahradením sa prepíše jeho obsah. replace-warning-operation = Chcete ho nahradiť? Nahradením sa prepíše jeho obsah. original-file = Pôvodný súbor replace-with = Nahradiť s apply-to-all = Použiť na všetky keep-both = Ponechať oboje skip = Preskočiť ## Set as Executable and Launch Dialog set-executable-and-launch = Nastaviť ako spustiteľné a spustiť set-executable-and-launch-description = Chcete nastaviť "{ $name }" ako spustiteľné a spustiť ho? set-and-launch = Nastaviť a spustiť ## Metadata Dialog open-with = Otvoriť pomocou owner = Vlastník group = Skupina other = Ostatní ### Mode 0 none = Žiadne ### Mode 1 (unusual) execute-only = Len spúšťanie ### Mode 2 (unusual) write-only = Len zápis ### Mode 3 (unusual) write-execute = Zápis a spúšťanie ### Mode 4 read-only = Len čítanie ### Mode 5 read-execute = Čítanie a spúšťanie ### Mode 6 read-write = Čítanie a zápis ### Mode 7 read-write-execute = Čítanie, zápis a spúšťanie ## Favorite Path Error Dialog favorite-path-error = Chyba pri otváraní adresára favorite-path-error-description = Nepodarilo sa otvoriť "{ $path }". Možno neexistuje alebo nemáte povolenie na jeho otvorenie. Chcete ho odstrániť z bočného panela? remove = Odstrániť keep = Ponechať # Context Pages ## About ## Add Network Drive add-network-drive = Pridať sieťový disk connect = Pripojiť connect-anonymously = Pripojiť anonymne connecting = Pripájanie... domain = Doména enter-server-address = Zadajte adresu servera network-drive-description = Adresy serverov obsahujú prefix protokolu a adresu. Príklady: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Dostupné protokoly,Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// alebo ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// alebo ssh:// WebDAV,dav:// alebo davs:// network-drive-error = Nie je možné získať prístup k sieťovému disku password = Heslo remember-password = Zapamätať heslo try-again = Skúsiť znova username = Používateľské meno ## Operations cancelled = Zrušené edit-history = Upraviť históriu history = História no-history = Žiadne položky v histórii. pending = Čakajúce progress = { $percent }% progress-cancelled = { $percent }%, zrušené progress-paused = { $percent }%, pozastavené failed = Zlyhalo complete = Dokončené compressing = Komprimujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" ({ $progress })... compressed = Komprimované { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" copy_noun = Kopírovať creating = Vytváram "{ $name }" v "{ $parent }" created = Vytvorené "{ $name }" v "{ $parent }" copying = Kopírujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" ({ $progress })... copied = Skopírované { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" deleting = Odstraňujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z { trash } ({ $progress })... deleted = Odstránené { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z { trash } emptying-trash = Vyprázdňujem { trash } ({ $progress })... emptied-trash = Kôš bol vyprázdnený extracting = Extrahujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" ({ $progress })... extracted = Extrahované { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" setting-executable-and-launching = Nastavujem "{ $name }" ako spustiteľné a spúšťam set-executable-and-launched = "{ $name }" nastavené ako spustiteľné a spustené setting-permissions = Nastavujem oprávnenia pre "{ $name }" na { $mode } set-permissions = Oprávnenia pre "{ $name }" nastavené na { $mode } moving = Presúvam { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" ({ $progress })... moved = Presunuté { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z "{ $from }" do "{ $to }" permanently-deleting = Trvalo odstraňujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } permanently-deleted = Trvalo odstránené { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } removing-from-recents = Odstraňujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z { recents } removed-from-recents = Odstránené { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z { recents } renaming = Premenovávam "{ $from }" na "{ $to }" renamed = Premenované "{ $from }" na "{ $to }" restoring = Obnovujem { $items } { $items -> [one] položku [few] položky [many] položiek *[other] položky } z { trash } ({ $progress })... restored = Obnovené { $items } { $items -> [one] položka [few] položky [many] položiek *[other] položky } z { trash } unknown-folder = neznámy priečinok ## Open with menu-open-with = Otvoriť pomocou... default-app = { $name } (predvolené) ## Show details show-details = Zobraziť podrobnosti type = Typ: { $mime } items = Položky: { $items } item-size = Veľkosť: { $size } item-created = Vytvorené: { $created } item-modified = Upravené: { $modified } item-accessed = Prístup: { $accessed } calculating = Vypočítavam... ## Settings settings = Nastavenia single-click = Otvoriť jedným kliknutím ### Appearance appearance = Vzhľad theme = Téma match-desktop = Prispôsobiť pracovnej ploche dark = Tmavá light = Svetlá ### Type to Search type-to-search = Hľadať písaním type-to-search-recursive = Prehľadáva aktuálny priečinok a všetky podpriečinky type-to-search-enter-path = Zadajte cestu k adresáru alebo súboru # Context menu add-to-sidebar = Pridať do bočného panela compress = Komprimovať delete-permanently = Trvalo odstrániť eject = Vysunúť extract-here = Extrahovať sem new-file = Nový súbor... new-folder = Nový priečinok... open-in-terminal = Otvoriť v termináli move-to-trash = Presunúť do koša restore-from-trash = Obnoviť z koša remove-from-sidebar = Odstrániť z bočného panela sort-by-name = Zoradiť podľa názvu sort-by-modified = Zoradiť podľa úpravy sort-by-size = Zoradiť podľa veľkosti sort-by-trashed = Zoradiť podľa času odstránenia remove-from-recents = Odstrániť z nedávnych ## Desktop change-wallpaper = Zmeniť tapetu... desktop-appearance = Vzhľad pracovnej plochy... display-settings = Nastavenia zobrazenia... # Menu ## File file = Súbor new-tab = Nová záložka new-window = Nové okno reload-folder = Obnoviť priečinok rename = Premenovať... close-tab = Zavrieť záložku quit = Ukončiť ## Edit edit = Upraviť cut = Vystrihnúť copy = Kopírovať paste = Prilepiť select-all = Vybrať všetko ## View zoom-in = Priblížiť default-size = Predvolená veľkosť zoom-out = Oddialiť view = Zobraziť grid-view = Zobrazenie mriežky list-view = Zobrazenie zoznamu show-hidden-files = Zobraziť skryté súbory list-directories-first = Najskôr priečinky gallery-preview = Náhľad galérie menu-settings = Nastavenia... menu-about = O aplikácii Súbory COSMIC... ## Sort sort = Zoradiť sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Najnovšie najskôr sort-oldest-first = Najstaršie najskôr sort-smallest-to-largest = Od najmenších po najväčšie sort-largest-to-smallest = Od najväčších po najmenšie repository = Repozitár support = Podpora progress-failed = { $percent }%, zlyhalo ================================================ FILE: i18n/sl/cosmic_files.ftl ================================================ ================================================ FILE: i18n/sr/cosmic_files.ftl ================================================ ================================================ FILE: i18n/sr-Cyrl/cosmic_files.ftl ================================================ empty-folder = Празна фасцикла empty-folder-hidden = Празна фасцикла (има скривене ставке) filesystem = Систем датотека trash = Отпад # Context Pages ## Properties ## Settings settings = Подешавања ### Appearance appearance = Изглед theme = Тема match-desktop = Као систем dark = Тамна light = Светла # Context menu new-file = Нова датотека new-folder = Нова фасцикла move-to-trash = Премести у отпад restore-from-trash = Врати из отпада # Menu ## File file = Датотека new-tab = Нова картица new-window = Нови прозор close-tab = Затвори картицу quit = Изађи ## Edit edit = Уреди cut = Исеци copy = Копирај paste = Налепи select-all = Изабери све ## View view = Приказ grid-view = Прикажи мрежу list-view = Прикажи списак menu-settings = Подешавања... cosmic-files = COSMIC Фајлови open-file = Отвори фајл cancel = Прекини repository = Репозиторијум support = Подршка no-results = Није пронађен ниједан резултат home = Кућа open-folder = Отвори директоријум password = Шифра networks = Мреже notification-in-progress = Операције над фајловима су у току. skip = Прескочи recents = Скорије undo = Поништи промену today = Данас desktop-view-options = Опције изгледа радне површине... show-on-desktop = Покажи на радној површини desktop-folder-content = Садржај директоријума радне површине mounted-drives = Приључена складишта података trash-folder-icon = Иконица корпе са отпаткама icon-size-and-spacing = Величина иконице и размак icon-size = Величина иконице ================================================ FILE: i18n/sr-Latn/cosmic_files.ftl ================================================ empty-folder = Prazna fascikla empty-folder-hidden = Prazna fascikla (ima skrivene stavke) filesystem = Sistem datoteka trash = Otpad # Context Pages ## Properties ## Settings settings = Podešavanja ### Appearance appearance = Izgled theme = Tema match-desktop = Kao sistem dark = Tamna light = Svetla # Context menu new-file = Nova datoteka new-folder = Nova fascikla move-to-trash = Premesti u otpad restore-from-trash = Vrati iz otpada # Menu ## File file = Datoteka new-tab = Nova kartica new-window = Novi prozor close-tab = Zatvori karticu quit = Izađi ## Edit edit = Uredi cut = Iseci copy = Kopiraj paste = Nalepi select-all = Izaberi sve ## View view = Prikaz grid-view = Prikaži mrežu list-view = Prikaži spisak menu-settings = Podešavanja... repository = Repozitorijum support = Podrška cancel = Poništi zoom-in = Uvećaj default-size = Podrazumevana veličina zoom-out = Umanji ================================================ FILE: i18n/sv/cosmic_files.ftl ================================================ cosmic-files = COSMIC Filer comment = Filhanterare för skrivbordsmiljön COSMIC keywords = Folder;Katalog;Mapp;Manager; empty-folder = Mappen är tom empty-folder-hidden = Mappen är tom (har dolda objekt) no-results = Inga resultat hittades filesystem = Filsystem home = Hem networks = Nätverk notification-in-progress = Filåtgärder pågår trash = Papperskorg recents = Senaste undo = Ångra today = Idag # Skrivbordsvyalternativ desktop-view-options = Skrivbordsvyalternativ... show-on-desktop = Visa på skrivbord desktop-folder-content = Skrivbordsmappinnehåll mounted-drives = Monterade enheter trash-folder-icon = Ikon för papperskorgen icon-size-and-spacing = Ikonstorlek och mellanrum icon-size = Ikonstorlek # Dialogruta # Dialogrutor ## Komprimera dialogruta create-archive = Skapa arkiv ## Töm papperskorgen dialogruta empty-trash = Töm papperskorgen empty-trash-warning = Objekt i papperskorgen kommer att tas bort permanent ## Monteringsfel dialogruta mount-error = Kan inte komma åt enheten ## Ny Fil/katalog dialogruta create-new-file = Skapa ny fil create-new-folder = Skapa ny mapp file-name = Filnamn folder-name = Mappnamn file-already-exists = En fil med det namnet finns redan folder-already-exists = En mapp med det namnet finns redan name-hidden = Namn som börjar med "." kommer att vara dolda name-invalid = Namnet får inte vara "{ $filename }" name-no-slashes = Namnet får inte innehålla snedstreck ## Öppna/Spara dialogruta cancel = Avbryt create = Skapa open = Öppna open-file = Öppna fil open-folder = Öppna mapp open-in-new-tab = Öppna i en ny flik open-in-new-window = Öppna i nytt fönster open-item-location = Öppna objektets plats open-multiple-files = Öppna flera filer open-multiple-folders = Öppna flera mappar save = Spara save-file = Spara fil ## Öppna med dialogruta open-with-title = Hur vill du öppna "{ $name }"? browse-store = Bläddra i { $store } ## Byt namn dialogruta rename-file = Byt namn på fil rename-folder = Byt namn på mapp ## Ersätt dialogruta replace = Byt ut replace-title = "{ $filename }" finns redan på den här platsen replace-warning = Vill du ersätta filen med den du sparar? Om du ersätter den kommer dess innehåll att skrivas över. replace-warning-operation = Vill du ersätta den? Om du ersätter den kommer dess innehåll att skrivas över. original-file = Originalfil replace-with = Ersätt med apply-to-all = Verkställ för alla keep-both = Behåll båda skip = Hoppa över ## Ställ in som körbar och starta dialogruta set-executable-and-launch = Gör körbar och starta set-executable-and-launch-description = Vill du göra "{ $name }" körbar och starta den? set-and-launch = Ställ in och starta ## Metadata dialogruta open-with = Öppna med owner = Ägare group = Grupp other = Andra mixed = Blandade # Listvy name = Namn modified = Ändrad trashed-on = Kastad size = Storlek # Framstegssidfot details = Detaljer dismiss = Avfärda meddelande operations-running = { $running } { $running -> [one] åtgärd *[other] åtgärder } kör ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] åtgärd *[other] åtgärder } kör ({ $percent }%), { $finished } slutförda... pause = Pausa resume = Återuppta # Kontextsidor ## Om ## Lägg till en Nätverksenhet add-network-drive = Lägg till en Nätverksenhet connect = Anslut connect-anonymously = Anslut anonymt connecting = Ansluter... domain = Domän enter-server-address = Ange serveradress try-again = Försök igen username = Användarnamn network-drive-description = Serveradresser består av ett protokollprefix och en adress. Exempel: ssh://192.168.0.1, ftp://[2001:db8::1] ### Se till att behålla kommatecken som skiljer kolumnerna åt network-drive-schemes = Tillgängliga protokoll, Prefix AppleTalk,afp:// File Transfer Protocol,ftp:// eller ftps:// Network File System (NFS),nfs:// Server Message Block (SMB),smb:// SSH-filöverföringsprotokoll,sftp:// eller ssh:// WebDav,dav:// eller davs:// network-drive-error = Kan inte komma åt nätverksenheten password = Lösenord remember-password = Kom ihåg lösenord ## Operationer cancelled = Avbruten edit-history = Redigera historik history = Historik no-history = Inga objekt i historiken. pending = Väntar progress = { $percent }% progress-cancelled = { $percent }%, avbruten progress-paused = { $percent }%, pausad failed = Misslyckad complete = Färdig compressing = Komprimerar { $items } { $items -> [one] objekt *[other] objekt } från "{ $from }" till "{ $to }" ({ $progress })... compressed = Komprimerade { $items } { $items -> [one] objekt *[other] objekt } från "{ $from }" till "{ $to }" copy_noun = Kopiera creating = Skapar "{ $name }" i "{ $parent }" created = Skapade "{ $name }" i "{ $parent }" copying = Kopierar { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" ({ $progress })... copied = Kopierade { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" emptying-trash = Tömmer { trash } ({ $progress })... emptied-trash = Tömde { trash } extracting = Packar upp { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" ({ $progress })... extracted = Packade upp { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" setting-executable-and-launching = Gör "{ $name }" körbar och startar set-executable-and-launched = Gjorde "{ $name }" körbar och startade moving = Flyttar { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" ({ $progress })... moved = Flyttade { $items } { $items -> [one] objekt *[other] flera objekt } från "{ $from }" till "{ $to }" renaming = Byter namn på "{ $from }" till "{ $to }" renamed = Bytt namn på "{ $from }" till "{ $to }" restoring = Återställer { $items } { $items -> [one] objekt *[other] flera objekt } från { trash } ({ $progress })... restored = Återställt { $items } { $items -> [one] objekt *[other] flera objekt } från { trash } unknown-folder = okänd mapp ## Öppna med menu-open-with = Öppna med... default-app = { $name } (standard) ## Visa detaljer show-details = Visa detaljer type = Typ: { $mime } items = Objekt: { $items } item-size = Storlek: { $size } item-created = Skapad: { $created } item-modified = Ändrad: { $modified } item-accessed = Åtkomst: { $accessed } calculating = Beräknar... ## Egenskaper ## Inställningar settings = Inställningar single-click = Ett enkelklick för att öppna ### Utseende appearance = Utseende theme = Tema match-desktop = Matcha skrivbordet dark = Mörkt light = Ljust ### Skriv för att söka type-to-search = Skriv för att söka type-to-search-recursive = Söker i den aktuella mappen och alla undermappar type-to-search-enter-path = Anger sökvägen till mappen eller filen # Kontext meny add-to-sidebar = Lägg till i sidofält compress = Komprimera... extract-here = Packa upp new-file = Ny fil… new-folder = Ny mapp… open-in-terminal = Öppna i terminal move-to-trash = Flytta till papperskorg restore-from-trash = Återställ från papperskorgen remove-from-sidebar = Ta bort från sidofält sort-by-name = Sortera efter namn sort-by-modified = Sortera efter senast ändrad sort-by-size = Sortera efter storlek sort-by-trashed = Sortera efter borttagningstid ## Skrivbord change-wallpaper = Byt bakgrund... desktop-appearance = Skrivbordsutseende... display-settings = Skärminställningar... # Meny ## Fil file = Fil new-tab = Ny flik new-window = Nytt fönster rename = Byt namn... close-tab = Stäng flik quit = Avsluta ## Redigera edit = Redigera cut = Klipp ut copy = Kopiera paste = Klistra in select-all = Välj alla ## Visa zoom-in = Zooma in default-size = Standardstorlek zoom-out = Zooma ut view = Visa grid-view = Rutnätsvy list-view = Listvy show-hidden-files = Visa dolda filer list-directories-first = Lista mappar först gallery-preview = Galleri förhandsvisning menu-settings = Inställningar… menu-about = Om COSMIC Filer... ## Sortera sort = Sortera sort-a-z = A-Ö sort-z-a = Ö-A sort-newest-first = Nyaste först sort-oldest-first = Äldst först sort-smallest-to-largest = Minsta till största sort-largest-to-smallest = Största till minsta remove = Ta bort repository = Källkod support = Support grid-spacing = Rutnätsmellanrum extract-password-required = Lösenord krävs extract-to = Packa upp till... extract-to-title = Packa upp till mapp other-apps = Andra program related-apps = Relaterade program permanently-delete-question = Ta bort permanent? delete = Ta bort permanently-delete-warning = { $target } kommer att tas bort permanent. Detta kan inte göras ogjort. none = Ingen execute-only = Endast exekvera write-only = Endast skriva write-execute = Skriva och exekvera read-only = Endast läsa read-execute = Läsa och exekvera read-write = Läsa och skriva read-write-execute = Läsa, skriva och exekvera favorite-path-error = Fel vid öppning av mapp favorite-path-error-description = Kunde inte öppna "{ $path }" "{ $path }" finns kanske inte eller så har du inte behörighet att öppna den Vill du ta bort den från sidolisten? keep = Behåll progress-failed = { $percent }%, misslyckades deleting = Raderar { $items } { $items -> [one] objekt *[other] objekt } från { trash } ({ $progress })... deleted = Borttagna { $items } { $items -> [one] objekt *[other] objekt } från { trash } setting-permissions = Sätter behörigheter för "{ $name }" till { $mode } set-permissions = Satte behörigheter för "{ $name }" to { $mode } permanently-deleting = Raderar { $items } { $items -> [one] objekt *[other] objekt } permanent permanently-deleted = Permanent borttagna { $items } { $items -> [one] objekt *[other] objekt } removing-from-recents = Tar bort { $items } { $items -> [one] objekt *[other] objekt } från { recents } removed-from-recents = Tog bort { $items } { $items -> [one] objekt *[other] objekt } från { recents } delete-permanently = Ta bort permanent eject = Mata ut remove-from-recents = Ta bort från senaste reload-folder = Ladda om mapp selected-items = De { $items } valda objekten empty-trash-title = Töm papperskorgen? type-to-search-select = Markerar den första matchande filen eller mappen pasted-image = Inklistrad bild pasted-text = Inklistrad text pasted-video = Inklistrad video copy-to-title = Välj mål för kopiering copy-to-button-label = Kopiera move-to-title = Välj mål för flytt move-to-button-label = Flytta copy-to = Kopiera till... 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 ================================================ FILE: i18n/ta/cosmic_files.ftl ================================================ ================================================ FILE: i18n/th/cosmic_files.ftl ================================================ cosmic-files = ตัวจัดการไฟล์ COSMIC empty-folder = แฟ้มเปล่า empty-folder-hidden = แฟ้มเปล่า (มีแฟ้มที่ซ่อนอยู่) no-results = ไม่พบผลลัพธ์ filesystem = ระบบไฟล์ home = บ้าน networks = เครือข่าย notification-in-progress = กำลังดำเนินการไฟล์ trash = ถังขยะ recents = ล่าสุด undo = เลิกทำ today = วันนี้ # Desktop view options desktop-view-options = ตัวเลือกมุมมองหน้าจอหลัก... show-on-desktop = แสดงบนหน้าจอหลัก desktop-folder-content = เนื้อหาแฟ้มหน้าจอหลัก mounted-drives = ไดร์ฟที่ใช้งานได้ trash-folder-icon = ไอคอนแฟ้มถังขยะ icon-size-and-spacing = ขนาดและระยะห่างไอคอน icon-size = ขนาดไอคอน # List view name = ชื่อ modified = แก้ไขล่าสุด trashed-on = ถูกทิ้ง size = ขนาด # Progress footer details = รายละเอียด dismiss = ไม่สนใจข้อความ operations-running = การดำเนินการ { $running } running ({ $percent }%)... operations-running-finished = { $running } operations running ({ $percent }%), { $finished } finished... pause = หยุด resume = ทำต่อ # Dialogs ## Compress Dialog create-archive = สร้างไฟล์บีบอัด ## Empty Trash Dialog empty-trash = ล้างถังขยะ empty-trash-warning = คุณแน่ใจหรือไม่ว่าคุณต้องการจะลบภายในถังขยะถาวร ## Mount Error Dialog mount-error = ไม่สามารถเข้าถึงไดร์ฟได้ ## New File/Folder Dialog create-new-file = สร้างไฟล์ใหม่ create-new-folder = สร้างแฟ้มใหม่ file-name = ชื่อไฟล์ folder-name = ชื่อแฟ้ม file-already-exists = มีไฟล์ชื่อนี้อยู่แล้ว folder-already-exists = มีแฟ้มชื่อนี้อยู่แล้ว name-hidden = ชื่อที่ขึ้นต้นด้วย "." จะถูกซ่อน name-invalid = ไม่สามารถตั้ง "{ $filename }" เป็นชื่อได้ name-no-slashes = ชื่อไม่สามารถมีเครื่องหมายทับได้ ## Open/Save Dialog cancel = ยกเลิก create = สร้าง open = เปิด open-file = เปิดไฟล์ open-folder = เปิดแฟ้ม open-in-new-tab = เปิดในแทบใหม่ open-in-new-window = เปิดในหน้าต่างใหม่ open-item-location = เปิดตำแหน่งของรายการ open-multiple-files = เปิดหลายไฟล์ open-multiple-folders = เปิดหลายแฟ้ม save = บันทึก save-file = บักทึกไฟล์ ## Open With Dialog open-with-title = คุณจะเปิดไฟล์ "{ $name }" อย่างไร browse-store = เรียกดูใน { $store } ## Rename Dialog rename-file = เปลี่ยนชื่อไฟล์ rename-folder = เปลี่ยนชื่อแฟ้ม ## Replace Dialog replace = แทนที่ replace-title = มี "{ $filename }" อยู่แล้วที่ตำแหน่งนี้ replace-warning = คุณต้องการจะแทนที่ไฟล์ด้วยไฟล์ที่คุณกำลังบันทึกอยู่หรือไม่ การแทนที่จะเขียนทับเนื้อหาเดิม replace-warning-operation = คุณต้องการจะแทนที่ไฟล์หรือไม่ การแทนที่จะเขียนทับเนื้อหาเดิม original-file = ไฟล์ต้นฉบับ replace-with = แทนที่ด้วย apply-to-all = นำไปใช้กับทั้งหมด keep-both = เก็บไว้ทั้งคู่ skip = ข้าม ## Set as Executable and Launch Dialog set-executable-and-launch = ตั้งเป็นไฟล์ที่สามารถรันได้และเปิด set-executable-and-launch-description = คุณต้องการที่จะตั้งไฟล์ "{ $name }" ให้สามารถรันได้และเปิดเลยหรือไม่ set-and-launch = ตั้งและเปิด ## Metadata Dialog owner = เจ้าของ group = กลุ่ม other = ผู้อื่น # Context Pages ## About ## Add Network Drive add-network-drive = เพิ่มไดรฟ์เครือข่าย connect = เชื่อมต่อ connect-anonymously = เชื่อมต่อแบบไม่ระบุตัวตน connecting = กำลังเชื่อมต่อ... domain = โดเมน enter-server-address = ใส่ที่อยู่เซิร์ฟเวอร์ network-drive-description = ที่อยู่ของเซิร์ฟเวอร์ประกอบด้วยโปรโตคอลและที่อยู่ เช่น: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = โปรโตคอลที่ใช้งานได้,โปรโตคอล AppleTalk,afp:// File Transfer Protocol,ftp:// or ftps:// Network File System,nfs:// Server Message Block,smb:// SSH File Transfer Protocol,sftp:// or ssh:// WebDav,dav:// or davs:// network-drive-error = ไม่สามารถเข้าถึงไดร์ฟเครือข่าย password = รหัสผ่าน remember-password = จดจำรหัสผ่าน try-again = ลองอีกครั้ง username = ชื่อผู้ใช้ ## Operations cancelled = ยกเลิกแล้ว edit-history = แก้ไขประวัติ history = ประวัติ no-history = ไม่มีไฟล์ในประวัติ pending = รอดำเนินการ progress = { $percent }% progress-cancelled = { $percent }%, ยกเลิกแล้ว progress-paused = { $percent }%, หยุดชั่วคราว failed = ล้มเหลว complete = เสร็จสิ้น compressing = กำลังบีบอัด { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" ({ $progress })... compressed = บีบอัด { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" copy_noun = คัดลอก creating = กำลังสร้างไฟล์ "{ $name }" ใน "{ $parent } created = สร้าง "{ $name }" ใน "{ $parent }" แล้ว copying = กำลังคำลอก { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" ({ $progress })... copied = เสร็จสิ้นการคัดลอก { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" emptying-trash = กำลังล้างถังขยะ { trash } ไฟล์ ({ $progress })... emptied-trash = ล้างถังขยะแล้ว { trash } ไฟล์ extracting = กำลังแตกไฟล์ { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" ({ $progress })... extracted = เสร็จสิ้นการแตกไฟล์ { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" setting-executable-and-launching = กำลังตั้งไฟล์ "{ $name }" ให้สามารถรันได้และเปิด set-executable-and-launched = ตั้งไฟล์ "{ $name }" ให้สามารถรันได้และเปิดแล้ว moving = กำลังย้ายไฟล์ { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" ({ $progress })... moved = เสร็จสิ้นการย้ายไฟล์ { $items } { $items -> [one] ไฟล์ *[other] ไฟล์ } จาก "{ $from }" สู่ "{ $to }" renaming = กำลังเปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }" renamed = Renamed "{ $from }" to "{ $to }" restoring = Restoring { $items } { $items -> [one] item *[other] items } from { trash } ({ $progress })... restored = Restored { $items } { $items -> [one] item *[other] items } from { trash } unknown-folder = แฟ้มที่ไม่รู้จัก ## Open with menu-open-with = เปิดด้วย... default-app = { $name } (ค่าเริ่มต้น) ## Show details show-details = แสดงรายละเอียด type = ชนิด: { $mime } items = ไฟล์: { $items } item-size = ขนาดไฟล์: { $size } item-created = สร้างเมื่อ: { $created } item-modified = แก้ไขเมื่อ: { $modified } item-accessed = เปิดใช้เมื่อ: { $accessed } calculating = กำลังคำนวณ... ## Settings settings = การตั้งค่า ### Appearance appearance = ลักษณะ theme = ธีม match-desktop = ใช้ตามธีมหน้าจอหลัก dark = ธีมมืด light = ธีมสว่าง # Context menu add-to-sidebar = เพิ่มเข้าแถบด้านข้าง compress = บีบอัด extract-here = แตกไฟล์ new-file = สร้างไฟล์... new-folder = สร้างแฟ้ม... open-in-terminal = เปิดในเทอร์มินัล move-to-trash = ย้ายไปถังขยะ restore-from-trash = เรียกคืนจากถังขยะ remove-from-sidebar = นำออกจากแถบด้านข้าง sort-by-name = เรียงตามชื่อ sort-by-modified = เรียงตามเวลาแก้ไขล่าสุด sort-by-size = เรียงตามขนาด sort-by-trashed = เรียงตามเวลาลบ ## Desktop change-wallpaper = เปลี่ยนภาพพื้นหลัง... desktop-appearance = ลักษณะหน้าจอหลัก... display-settings = การตั้งค่าหน้าจอแสดงผล... # Menu ## File file = ไฟล์ new-tab = แทบใหม่ new-window = หน้าต่างใหม่ rename = เปลี่ยนชื่อ... close-tab = ปิดแทบ quit = ออก ## Edit edit = แก้ไข cut = ตัด copy = คัดลอก paste = วาง select-all = เลือกทั้งหมด ## View zoom-in = ซูมเข้า default-size = ขนาดดั้งเดิม zoom-out = ซูมออก view = มุมมอง grid-view = มุมมองแบบตาราง list-view = มุมมองแบบรายการ show-hidden-files = แสดงไฟล์ที่ซ่อนอยู่ list-directories-first = แสดงแฟ้มก่อนเสมอ gallery-preview = ตัวอย่างแบบแกลเลอรี่ menu-settings = การตั้งค่า... 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 = ขนาดใหญ่ก่อน ================================================ FILE: i18n/ti/cosmic_files.ftl ================================================ ================================================ FILE: i18n/tr/cosmic_files.ftl ================================================ cosmic-files = COSMIC Dosyalar empty-folder = Boş klasör empty-folder-hidden = Boş klasör (gizli ögeler içerir) no-results = Sonuç bulunamadı filesystem = Dosya sistemi home = Ev networks = Ağlar notification-in-progress = Dosya işlemi devam etmekte trash = Çöp recents = Son kullanılanlar undo = Geri al today = Bugün # Desktop view options desktop-view-options = Masaüstü görünüm seçenekleri... show-on-desktop = Masaüstünde göster desktop-folder-content = Masaüstü klasörü içeriği mounted-drives = Bağlı sürücüler trash-folder-icon = Çöp klasörü simgesi icon-size-and-spacing = Simge boyutu ve boşluğu icon-size = Simge boyutu # List view name = Ad modified = Değiştirildi trashed-on = Çöpe atılma size = Boyut # Progress footer details = Detaylar dismiss = Mesajı kapat operations-running = { $running } { $running -> [one] işlem *[other] işlem } çalışıyor ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] işlem *[other] işlem } çalışıyor ({ $percent }%), { $finished } bitti... pause = Duraklat resume = Devam et # Dialogs ## Compress Dialog create-archive = Arşiv oluştur ## Empty Trash Dialog empty-trash = Çöpü boşalt empty-trash-warning = Çöp Kutusu klasöründeki öğeler kalıcı olarak silinecektir ## Mount Error Dialog mount-error = Sürücüye erişilemedi ## New File/Folder Dialog create-new-file = Yeni dosya oluştur create-new-folder = Yeni klasör oluştur file-name = Dosya adı folder-name = Klasör adı file-already-exists = Bu isimde bir dosya zaten var folder-already-exists = Bu isimde bir klasör zaten var name-hidden = "." ile başlayan adlar gizlenecek name-invalid = Ad "{ $filename }" olamaz name-no-slashes = Ad eğik çizgi içeremez ## Open/Save Dialog cancel = Vazgeç create = Oluştur open = Aç open-file = Dosya aç open-folder = Klasör aç open-in-new-tab = Yeni sekmede aç open-in-new-window = Yeni pencerede aç open-item-location = Öge konumunu aç open-multiple-files = Birden fazla dosyayı aç open-multiple-folders = Birden fazla klasörü aç save = Kaydet save-file = Dosyayı kaydet ## Open With Dialog open-with-title = "{ $name }" dosyasını nasıl açmak istersiniz? browse-store = { $store }'sını gezin ## Rename Dialog rename-file = Dosyayı yeniden adlandır rename-folder = Klasörü yeniden adlandır ## Replace Dialog replace = Değiştir replace-title = "{ $filename }" bu konumda zaten var replace-warning = Kaydettiğiniz dosya ile değiştirmek istiyor musunuz? Değiştirme içeriğin üzerine yazacak. replace-warning-operation = Değiştirmek istiyor musunuz? Değiştirme içeriğin üzerine yazacak. original-file = Orijinal dosya replace-with = Bununla değiştir apply-to-all = Tümüne uygula keep-both = İkisini de sakla skip = Atla ## Set as Executable and Launch Dialog set-executable-and-launch = Çalıştırılabilir olarak ayarla ve başlat set-executable-and-launch-description = "{ $name }" dosyasını çalıştırılabilir olarak ayarlayıp başlatmak istiyor musunuz? set-and-launch = Ayarla ve başlat ## Metadata Dialog owner = Sahibi group = Grup other = Diğer # Context Pages ## About ## Add Network Drive add-network-drive = Ağ sürücüsü ekle connect = Bağlan connect-anonymously = Anonim olarak bağlan connecting = Bağlanılıyor... domain = Alan enter-server-address = Sunucu adresi girin network-drive-description = Sunucu adresleri protokol ön eki ve adres içerir. Örneğin: ssh://192.168.0.1, ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = Kullanılabilir protokoller,Ön eki AppleTalk,afp:// Dosya Transfer Protokolü,ftp:// veya ftps:// Ağ Dosya Sistemi,nfs:// Sunucu Mesaj Bloğu,smb:// SSH Dosya Transfer Protokolü,sftp:// veya ssh:// WebDav,dav:// veya davs:// network-drive-error = Ağ sürücüsüne erişilemiyor password = Parola remember-password = Parolayı hatırla try-again = Tekrar dene username = Kullanıcı adı ## Operations cancelled = İptal edildi edit-history = Geçmişi düzenle history = Geçmiş no-history = Geçmişte öge bulunmuyor. pending = Devam ediyor progress = %{ $percent } progress-cancelled = %{ $percent }, iptal edildi progress-paused = %{ $percent }, duraklatıldı failed = Başarısız complete = Tamamlandı compressing = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e sıkıştırılıyor ({ $progress })... compressed = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e sıkıştırıldı copy_noun = Kopyala creating = "{ $parent }" de "{ $name }" oluşturuluyor created = "{ $parent }" de "{ $name }" oluşturuldu copying = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e kopyalanıyor ({ $progress })... copied = { $items } { $items -> [one] öge *[other] öge } "{ $from }"den "{ $to }" e kopyalandı emptying-trash = { trash } boşaltılıyor ({ $progress })... emptied-trash = { trash } boşaltıldı extracting = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e çıkartılıyor ({ $progress })... extracted = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e çıkartıldı setting-executable-and-launching = "{ $name }" çalıştırılabilir olarak ayarlanıp başlatılıyor set-executable-and-launched = "{ $name }" çalıştırılabilir olarak ayarlanıp başlatıldı moving = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e taşınıyor ({ $progress })... moved = { $items } { $items -> [one] öge *[other] öge } "{ $from }" den "{ $to }" e taşındı renaming = "{ $from }" adı "{ $to }" olarak değiştiriliyor renamed = "{ $from }" adı "{ $to }" olarak değiştirildi restoring = { $items } { $items -> [one] öge *[other] öge } { trash } den geri yükleniyor ({ $progress })... restored = { $items } { $items -> [one] öge *[other] öge } { trash } den geri yüklendi unknown-folder = bilinmeyen klasör ## Open with menu-open-with = Birlikte aç... default-app = { $name } (varsayılan) ## Show details show-details = Detayları göster type = Tür: { $mime } items = Öge sayısı: { $items } item-size = Boyut: { $size } item-created = Oluşturulma: { $created } item-modified = Düzenlenme: { $modified } item-accessed = Erişilme: { $accessed } calculating = Hesaplanıyor... ## Settings settings = Ayarlar ### Appearance appearance = Görünüm theme = Tema match-desktop = Masaüstü stilini takip et dark = Karanlık light = Aydınlık # Context menu add-to-sidebar = Kenar çubuğuna ekle compress = Sıkıştır extract-here = Çıkar new-file = Yeni dosya... new-folder = Yeni klasör... open-in-terminal = Uçbirimde aç move-to-trash = Çöpe taşı restore-from-trash = Çöpten geri yükle remove-from-sidebar = Kenar çubuğundan kaldır sort-by-name = Ada göre sırala sort-by-modified = Düzenlenme tarihine göre sırala sort-by-size = Boyuta göre sırala sort-by-trashed = Silme tarihine göre sırala ## Desktop change-wallpaper = Arka planı değiştir... desktop-appearance = Masaüstü görünümü... display-settings = Görüntü ayarları... # Menu ## File file = Dosya new-tab = Yeni sekme new-window = Yeni pencere rename = Yeniden adlandır... close-tab = Sekmeyi kapat quit = Çıkış ## Edit edit = Düzenle cut = Kes copy = Kopyala paste = Yapıştır select-all = Tümünü seç ## View zoom-in = Yakınlaştır default-size = Varsayılan boyut zoom-out = Uzaklaştır view = Görünüm grid-view = Tablo görünümü list-view = Liste görünümü show-hidden-files = Gizli dosyaları göster list-directories-first = Önce dizinleri listele gallery-preview = Galeri ön izlemesi menu-settings = Ayarlar... menu-about = COSMIC Dosyalar hakkında... ## Sort sort = Sırala sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Önce en yeni sort-oldest-first = Önce en eski sort-smallest-to-largest = En küçükten en büyüğe sort-largest-to-smallest = En büyükten en küçüğe repository = Depo support = Destek remove = Kaldır grid-spacing = Izgara aralığı extract-password-required = Parola gerekli extract-to = Buraya Çıkar… extract-to-title = Klasöre çıkar other-apps = Diğer uygulamalar related-apps = İlgili uygulamalar selected-items = { $items } seçili öğeler permanently-delete-question = Kalıcı olarak silinsin mi? delete = Sil permanently-delete-warning = { $target } kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz. open-with = Birlikte aç none = Yok execute-only = Yalnızca çalıştırma write-only = Yalnızca yazma write-execute = Yazma ve çalıştırma read-only = Yanlızca okuma read-execute = Okuma ve çalıştırma read-write = Okuma ve yazma read-write-execute = Okuma, yazma ve çalıştırma favorite-path-error = Dizin açılırken hata oluştu favorite-path-error-description = "{ $path }" açılamadı "{ $path }" Dosya mevcut olmayabilir veya açmak için izniniz olmayabilir. Bunu kenar çubuğundan kaldırmak ister misiniz? keep = Tut progress-failed = %{ $percent }, başarısız oldu deleting = Çöp kutusundan { $items } { $items -> [one] öge *[other] öge } { trash } ({ $progress }) siliniyor... deleted = Çöp kutusundan { $items } { $items -> [one] öge *[other] öge } { trash } silindi setting-permissions = "{ $name }" için izinler { $mode } olarak ayarlanıyor set-permissions = "{ $name }" için izinleri { $mode } olarak ayarla permanently-deleting = { $items } { $items -> [one] öge *[other] öge } kalıcı olarak siliniyor permanently-deleted = { $items } { $items -> [one] öge *[other] öge } kalıcı olarak silindi removing-from-recents = { $items } { $items -> [one] öge *[other] öge } { recents } den kaldırılıyor removed-from-recents = { $items } { $items -> [one] öge *[other] öge } { recents } den kaldırıldı single-click = Açmak için tek tıklama type-to-search = Aramak için yazın type-to-search-recursive = Geçerli klasörü ve tüm alt klasörleri arar type-to-search-enter-path = Dizin veya dosyanın yolunu girer delete-permanently = Kalıcı olarak sil eject = Çıkart remove-from-recents = Son kullanılanlardan kaldır reload-folder = Klasörü yeniden yükle comment = COSMIC masaüstü için dosya yöneticisi keywords = Klasör;Yönetici; empty-trash-title = Çöp boşaltılsın mı? copy-to-title = Kopyalama hedefini seçin copy-to-button-label = Kopyala move-to-title = Taşıma hedefini seçin move-to-button-label = Taşı pasted-image = Yapıştırılan Görüntü pasted-text = Yapıştırılan Metin pasted-video = Yapıştırılan Video clear-recents-history = Son kullanılanlar geçmişini temizle copy-path = Yolu kopyala type-to-search-select = İlk eşleşen dosyayı veya klasörü seçer copy-to = Buraya kopyala… move-to = Buraya taşı… show-recents = Kenar çubuğundaki son kullanılanlar klasörü ================================================ FILE: i18n/uk/cosmic_files.ftl ================================================ cosmic-files = Файли COSMIC empty-folder = Порожня тека empty-folder-hidden = Порожня тека (містить приховані елементи) filesystem = Файлова система home = Домівка trash = Смітник recents = Нещодавні undo = Відмінити # List view name = Назва modified = Змінено size = Розмір # Dialogs ## Empty Trash Dialog empty-trash = Спорожнити смітник empty-trash-warning = Елементи зі смітника будуть остаточно видалені ## New File/Folder Dialog create-new-file = Створити новий файл create-new-folder = Створити нову теку file-name = Назва файлу folder-name = Назва теки file-already-exists = Файл з такою назвою вже існує folder-already-exists = Тека з такою назвою вже існує name-hidden = Назви, що починаються з «.», приховано name-invalid = Назва не може бути «{ $filename }» name-no-slashes = Назва не може містити скісні риски ## Open/Save Dialog cancel = Скасувати open = Відкрити open-file = Відкрити файл open-folder = Відкрити теку open-in-new-tab = Відкрити в новій вкладці open-in-new-window = Відкрити в новому вікні open-multiple-files = Відкрити кілька файлів open-multiple-folders = Відкрити кілька тек save = Зберегти save-file = Зберегти файл ## Rename Dialog rename-file = Перейменувати файл rename-folder = Перейменувати теку ## Replace Dialog replace = Замінити replace-title = «{ $filename }» вже існує в цій теці replace-warning = Бажаєте замінити файл тим, що зберігаєте? Після заміни його вміст буде перезаписано. replace-warning-operation = Бажаєте замінити його? Це перезапише його вміст. original-file = Початковий файл replace-with = Замінити на apply-to-all = Застосувати до всіх keep-both = Залишити обидва skip = Пропустити # Context Pages ## About ## Operations edit-history = Історія дій history = Історія no-history = Історія порожня. pending = Очікується failed = Не виконано complete = Завершено copy_noun = Копіювати creating = Створення «{ $name }» в «{ $parent }» created = Створено «{ $name }» в «{ $parent }» copying = Копіювання { $items } { $items -> [one] елемента *[other] елементів } з «{ $from }» в «{ $to }» ({ $progress })... copied = Скопійовано { $items } { $items -> [one] елемент *[other] елеменів } з «{ $from }» в «{ $to }» emptying-trash = Спорожнення { trash } ({ $progress })... emptied-trash = Спорожнено { trash } moving = Переміщення { $items } { $items -> [one] елемента *[other] елементів } з «{ $from }» в «{ $to }» ({ $progress })... moved = Переміщено { $items } { $items -> [one] елемент *[other] елементи } з «{ $from }» в «{ $to }» renaming = Перейменування «{ $from }» на «{ $to }» renamed = Перейменовано «{ $from }» на «{ $to }» restoring = Відновлення { $items } { $items -> [one] елемента *[other] елементів } з { trash } ({ $progress })... restored = Відновлено { $items } { $items -> [one] елемент *[other] елементи } з { trash } unknown-folder = невідома тека ## Open with menu-open-with = Відкрити за допомогою... default-app = { $name } (звичайний) ## Properties ## Settings settings = Налаштування ### Appearance appearance = Вигляд theme = Тема match-desktop = Системна dark = Темна light = Світла # Context menu add-to-sidebar = Додати до бічної панелі new-file = Новий файл... new-folder = Нова тека... open-in-terminal = Відкрити у терміналі move-to-trash = Пересунути до смітника restore-from-trash = Відновити зі смітника remove-from-sidebar = Вилучити з бічної панелі sort-by-name = Упорядкувати за назвою sort-by-modified = Упорядкувати за зміною sort-by-size = Упорядкувати за розміром # Menu ## File file = Файл new-tab = Нова вкладка new-window = Нове вікно rename = Перейменувати... close-tab = Закрити вкладку quit = Вийти ## Edit edit = Редагувати cut = Вирізати copy = Копіювати paste = Вставити select-all = Вибрати все ## View zoom-in = Збільшити default-size = Стандартний розмір zoom-out = Зменшити view = Вид grid-view = Перегляд таблицею list-view = Перегляд списком show-hidden-files = Показати приховані файли list-directories-first = Теки спочатку menu-settings = Налаштування... menu-about = Про Файли COSMIC... repository = Сховище support = Підтримка details = Деталі dismiss = Закрити повідомлення remove = Вилучити cancelled = Скасовані no-results = Нічого не знайдено networks = Мережі notification-in-progress = Триває обробка файлів today = Сьогодні desktop-view-options = Параметри вигляду стільниці... show-on-desktop = Показувати на стільниці desktop-folder-content = Вміст теки Стільниця mounted-drives = Змонтовані диски trash-folder-icon = Піктограма Смітника icon-size-and-spacing = Розмір піктограм і відстань між ними icon-size = Розмір піктограм grid-spacing = Відстань між піктограмами trashed-on = У смітнику operations-running = { $running } { $running -> [one] операція *[other] операції } виконується ({ $percent }%)... operations-running-finished = { $running } { $running -> [one] операція *[other] операціі } виконується ({ $percent }%), { $finished } завершено... pause = Призупинити resume = Продовжити create-archive = Створити архів extract-password-required = Потрібен пароль extract-to = Видобути до... extract-to-title = Видобути до теки mount-error = Доступ до диска відсутній create = Створити open-item-location = Відкрити розташування елемента open-with-title = Як ви бажаєте відкрити «{ $name }»? browse-store = Переглянути { $store } other-apps = Інші застосунки related-apps = Пов'язані застосунки permanently-delete-question = Остаточно видалити? delete = Видалити permanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати. set-executable-and-launch = Зробити виконуваним і запустити set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його? set-and-launch = Зробити і запустити open-with = Відкрити за допомогою owner = Власник group = Група other = Інші none = Немає прав execute-only = Тільки виконання write-only = Тільки запис write-execute = Запис і виконання read-only = Тільки перегляд read-execute = Перегляд і виконання read-write = Перегляд і запис read-write-execute = Перегляд, запис і виконання favorite-path-error = Помилка при відкритті каталогу favorite-path-error-description = Не вдалося відкрити «{ $path }» «{ $path }» можливо не існує або у вас немає прав на відкриття Вилучити з бічної панелі? keep = Залишити add-network-drive = Додати мережевий диск connect = З'єднати connect-anonymously = З'єднатись анонімно connecting = З'єднання… domain = Домен enter-server-address = Введіть адресу сервера 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:// network-drive-error = Доступ до мережевого диска відсутній password = Пароль remember-password = Запам'ятати пароль try-again = Спробувати знову username = Ім'я користувача progress = { $percent }% progress-cancelled = { $percent }%, скасовано progress-failed = { $percent }%, не вдалося progress-paused = { $percent }%, призупинено compressing = Стиснення { $items } { $items -> [one] елемента *[other] елементів } з "{ $from }" до "{ $to }" ({ $progress })... compressed = Стиснуто { $items } { $items -> [one] елемент *[other] елементи } з "{ $from }" до "{ $to }" deleting = Видалення { $items } { $items -> [one] елемента *[other] елементів } з { trash } ({ $progress })... deleted = Видалено { $items } { $items -> [one] елемент *[other] елементи } з { trash } extracting = Видобування { $items } { $items -> [one] елемента *[other] елементів } з «{ $from }» в «{ $to }» ({ $progress })... extracted = Видобуто { $items } { $items -> [one] елемент *[other] елементи } з «{ $from }» в «{ $to }» setting-executable-and-launching = Надання «{ $name }» прав на виконання та запуск set-executable-and-launched = «{ $name }» надано права на виконання і відкрито selected-items = Вибрані { $items } елементи setting-permissions = Надання прав { $mode } для «{ $name }» set-permissions = Надано права { $mode } для «{ $name }» show-details = Показати подробиці type = Тип: { $mime } items = Елементів: { $items } item-size = Розмір: { $size } item-created = Створено: { $created } item-modified = Змінено: { $modified } item-accessed = Дата доступу: { $accessed } calculating = Обчислення... single-click = Відкривати одним клацанням type-to-search = Введіть для пошуку type-to-search-recursive = Шукає у поточній теці та всіх підтеках type-to-search-enter-path = Вводить шлях до каталогу або файлу compress = Стиснути... delete-permanently = Остаточно видалити eject = Безпечно вилучити extract-here = Видобути sort-by-trashed = Упорядкувати за часом видалення remove-from-recents = Вилучити з нещодавніх change-wallpaper = Змінити зображення тла... desktop-appearance = Вигляд стільниці... display-settings = Налаштування дисплея... reload-folder = Оновити теку gallery-preview = Попередній перегляд галереї sort = Упорядкувати sort-a-z = А-Я sort-z-a = Я-А sort-newest-first = Спочатку найновіші sort-oldest-first = Спочатку найстаріші sort-smallest-to-largest = Від найменшого до найбільшого sort-largest-to-smallest = Від найбільшого до найменшого permanently-deleting = Остаточне вилучення { $items } { $items -> [one] елемента *[other] елементів } permanently-deleted = Остаточно вилучено { $items } { $items -> [one] елемент *[other] елементи } removing-from-recents = Вилучення { $items } { $items -> [one] елемента *[other] елементів } з { recents } removed-from-recents = Вилучено { $items } { $items -> [one] елемент *[other] елементи } з { recents } empty-trash-title = Спорожити смітник? type-to-search-select = Вибирає перший відповідний файл або папку pasted-image = Вставлене Зображення pasted-text = Вставлений Текст pasted-video = Вставлене Видиво copy-to-button-label = Копіювати move-to-button-label = Перемістити copy-to = Копіювати до… move-to = Перемістити до… copy-to-title = Виберіть місце призначення move-to-title = Виберіть місце призначення comment = Менеджер файлів для середовища COSMIC keywords = Тека;Папка;Провідник;Менеджер;Каталог; show-recents = Тека «Нещодавні» на бічній панелі copy-path = Копіювати шлях clear-recents-history = Очистити нещодавні ================================================ FILE: i18n/uz/cosmic_files.ftl ================================================ ================================================ FILE: i18n/vi/cosmic_files.ftl ================================================ ================================================ FILE: i18n/yue-Hant/cosmic_files.ftl ================================================ ================================================ FILE: i18n/zh-CN/cosmic_files.ftl ================================================ cosmic-files = COSMIC 文件管理器 empty-folder = 空文件夹 empty-folder-hidden = 空文件夹(包含隐藏项目) no-results = 未找到结果 filesystem = 文件系统 home = 主页 networks = 网络 notification-in-progress = 文件操作正在进行中 trash = 回收站 recents = 最近访问 undo = 撤销 today = 今天 # Desktop view options desktop-view-options = 桌面视图选项… show-on-desktop = 在桌面显示 desktop-folder-content = 桌面文件夹内容 mounted-drives = 已装载驱动器 trash-folder-icon = 回收站图标 icon-size-and-spacing = 图标大小与间距 icon-size = 图标大小 grid-spacing = 网格间距 # List view name = 名称 modified = 修改时间 trashed-on = 删除时间 size = 大小 # Progress footer details = 详细信息 dismiss = 清除消息 operations-running = 正在进行 { $running } { $running -> [one] 个操作 *[other] 个操作 }({ $percent }%)… operations-running-finished = 正在进行 { $running } { $running -> [one] 个操作 *[other] 个操作 }({ $percent }%),{ $finished } 个操作已完成… pause = 暂停 resume = 继续 # Dialogs ## Compress Dialog create-archive = 创建压缩包 ## Extract Dialog extract-password-required = 需要密码 extract-to = 提取到… extract-to-title = 提取到文件夹 ## Empty Trash Dialog empty-trash = 清空回收站 empty-trash-warning = 回收站中的所有内容会被永久删除 ## Mount Error Dialog mount-error = 无法访问驱动器 ## New File/Folder Dialog create-new-file = 新建文件 create-new-folder = 新建文件夹 file-name = 文件名称 folder-name = 文件夹名称 file-already-exists = 同名文件已存在 folder-already-exists = 同名文件夹已存在 name-hidden = 以 “.” 开头的文件会被隐藏 name-invalid = 名称不能是 “{ $filename }” name-no-slashes = 名称不可以包含斜线 ## Open/Save Dialog cancel = 取消 create = 创建 open = 打开 open-file = 打开文件 open-folder = 打开文件夹 open-in-new-tab = 在新标签中打开 open-in-new-window = 在新窗口中打开 open-item-location = 打开项目位置 open-multiple-files = 打开多个文件 open-multiple-folders = 打开多个文件夹 save = 保存 save-file = 保存文件 ## Open With Dialog open-with-title = 您想要如何打开 “{ $name }”? browse-store = 浏览 { $store } other-apps = 其他应用程序 related-apps = 相关应用程序 ## Permanently delete Dialog selected-items = 选中的 { $items } 个项目 permanently-delete-question = 永久删除? delete = 删除 permanently-delete-warning = { $target } 将被永久删除。此操作无法撤销。 ## Rename Dialog rename-file = 重命名文件 rename-folder = 重命名文件夹 ## Replace Dialog replace = 替换 replace-title = “{ $filename }” 已存在于该位置 replace-warning = 您想要使用您现在正在保存的文件替换掉它吗?一旦替换将会覆盖其内容。 replace-warning-operation = 您想要替换掉它吗?一旦替换将会覆盖其内容。 original-file = 原始文件 replace-with = 替换为 apply-to-all = 全部应用 keep-both = 保留两者 skip = 跳过 ## Set as Executable and Launch Dialog set-executable-and-launch = 设置为可执行文件并启动 set-executable-and-launch-description = 您想要将 “{ $name }” 设置为可执行文件并启动它吗? set-and-launch = 设置并启动 ## Metadata Dialog open-with = 打开方式 owner = 所有者 group = 用户组 other = 其他用户 ### Mode 0 none = 无 ### Mode 1 (unusual) execute-only = 仅执行 ### Mode 2 (unusual) write-only = 仅写入 ### Mode 3 (unusual) write-execute = 写入和执行 ### Mode 4 read-only = 只读 ### Mode 5 read-execute = 读取和执行 ### Mode 6 read-write = 读取和写入 ### Mode 7 read-write-execute = 读取、写入和执行 ## Favorite Path Error Dialog favorite-path-error = 打开路径时出错 favorite-path-error-description = 无法打开 "{ $path }" 。 "{ $path }" 可能不存在或您没有权限打开它。 您想要从侧边栏中移除它吗? remove = 移除 keep = 保留 # Context Pages ## About ## Add Network Drive add-network-drive = 添加网络驱动器 connect = 连接 connect-anonymously = 匿名连接 connecting = 正在连接… domain = 网络域 enter-server-address = 输入服务器地址 network-drive-description = 服务器地址包含协议前缀和地址。 示例: ssh://192.168.0.1,ftp://[2001:db8::1] ### Make sure to keep the comma which separates the columns network-drive-schemes = 可用协议,前缀 AppleTalk,afp:// 文件传输协议,ftp:// 或者 ftps:// 网络文件系统,nfs:// 服务器消息块,smb:// SSH 文件传输协议,sftp:// 或者 ssh:// WebDav,dav:// 或者 davs:// network-drive-error = 无法访问网络驱动器 password = 密码 remember-password = 记住密码 try-again = 重试 username = 用户名 ## Operations cancelled = 已取消 edit-history = 编辑历史记录 history = 历史记录 no-history = 历史记录为空。 pending = 待处理 progress = { $percent }% progress-cancelled = { $percent }%,已取消 progress-paused = { $percent }%,已暂停 failed = 失败 complete = 完成 compressing = 正在从“{ $from }”压缩 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }”({ $progress })… compressed = 已从“{ $from }”压缩 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }” copy_noun = 复制 creating = 正在“{ $parent }”里创建“{ $name }” created = 已在“{ $parent }”里创建“{ $name }” copying = 正在从“{ $from }”复制 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }”({ $progress })… copied = 已从“{ $from }”复制 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }” deleting = 正在从{ trash }删除 { $items } { $items -> [one] 项目 *[other] 项目 }({ $progress })… deleted = 已从{ trash }删除 { $items } { $items -> [one] 项目 *[other] 项目 } emptying-trash = 正在清空{ trash }({ $progress })… emptied-trash = 已清空{ trash } extracting = 正在从“{ $from }”提取{ $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }”({ $progress })… extracted = 已从“{ $from }”提取 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }” setting-executable-and-launching = 设置 "{ $name }" 为可执行文件并启动 set-executable-and-launched = 已设 "{ $name }" 为可执行文件并且启动 setting-permissions = 正在设置 "{ $name }" 的权限为 { $mode } set-permissions = 设置 "{ $name }" 的权限为 { $mode } moving = 正在从“{ $from }”移动 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }”({ $progress })… moved = 已从“{ $from }”移动 { $items } { $items -> [one] 项目 *[other] 项目 }到“{ $to }” permanently-deleting = 正在永久删除 { $items } { $items -> [one] 项目 *[other] 项目 } permanently-deleted = 已永久删除 { $items } { $items -> [one] 项目 *[other] 项目 } removing-from-recents = 正在从{ recents }中移除 { $items } { $items -> [one] 项目 *[other] 项目 } removed-from-recents = 已从{ recents }中移除 { $items } { $items -> [one] 项目 *[other] 项目 } renaming = 正在将“{ $from }”重命名为“{ $to }” renamed = 已重命名"{ $from }"为"{ $to }" restoring = 正在从{ trash }中还原 { $items } { $items -> [one] 项目 *[other] 项目 }({ $progress })… restored = 已从{ trash }中还原 { $items } { $items -> [one] 项目 *[other] 项目 } unknown-folder = 未知文件夹 ## Open with menu-open-with = 打开方式… default-app = { $name }(默认) ## Show details show-details = 显示详情 type = 文件类型:{ $mime } items = 文件数:{ $items } item-size = 文件大小:{ $size } item-created = 创建于:{ $created } item-modified = 修改于:{ $modified } item-accessed = 访问于:{ $accessed } calculating = 计算中… ## Settings settings = 设置 single-click = 单击打开 ### Appearance appearance = 外观 theme = 主题 match-desktop = 匹配桌面 dark = 暗色模式 light = 亮色模式 ### Type to Search type-to-search = 输入即可搜索 type-to-search-recursive = 搜索当前文件夹及其所有子文件夹 type-to-search-enter-path = 输入文件夹或文件路径 # Context menu add-to-sidebar = 加入侧边栏 compress = 压缩… delete-permanently = 永久删除 eject = 弹出 extract-here = 解压到此处 new-file = 新建文件… new-folder = 新建文件夹… open-in-terminal = 在终端模拟器中打开 move-to-trash = 移动到回收站 restore-from-trash = 从回收站中还原 remove-from-sidebar = 从侧边栏中移除 sort-by-name = 按名称排序 sort-by-modified = 按修改时间排序 sort-by-size = 按文件大小排序 sort-by-trashed = 按删除时间排序 remove-from-recents = 从最近访问中移除 ## Desktop change-wallpaper = 更改壁纸… desktop-appearance = 桌面外观… display-settings = 显示设置… # Menu ## File file = 文件 new-tab = 新建标签 new-window = 新建窗口 reload-folder = 刷新文件夹 rename = 重命名… close-tab = 关闭标签 quit = 退出 ## Edit edit = 编辑 cut = 剪切 copy = 复制 paste = 粘贴 select-all = 全选 ## View zoom-in = 增大 default-size = 默认大小 zoom-out = 缩小 view = 视图 grid-view = 表格视图 list-view = 列表视图 show-hidden-files = 显示隐藏文件 list-directories-first = 优先列出目录 gallery-preview = 图库预览 menu-settings = 设置… 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 = 从大到小 repository = 仓库 support = 支持 progress-failed = { $percent }%,失败 empty-trash-title = 确定要清空回收站? type-to-search-select = 选择第一个匹配的文件或文件夹 pasted-video = 已粘贴视频 pasted-image = 已粘贴图片 pasted-text = 已粘贴文本 copy-to-title = 选择复制目的地 copy-to-button-label = 复制 move-to-title = 选择移动目的地 move-to-button-label = 移动 copy-to = 复制至… move-to = 移动至… comment = COSMIC 桌面的文件管理器 keywords = 文件夹;管理器; clear-recents-history = 清除最近访问历史 copy-path = 复制文件路径 show-recents = 侧边栏中的最近访问 mixed = 混合 ================================================ FILE: i18n/zh-TW/cosmic_files.ftl ================================================ cosmic-files = COSMIC 檔案 empty-folder = 空資料夾 empty-folder-hidden = 空資料夾(包含隱藏項目) no-results = 找不到結果 filesystem = 檔案系統 home = 家目錄 networks = 網路 notification-in-progress = 檔案操作正在進行中 trash = 垃圾桶 recents = 最近使用 undo = 復原 today = 今天 # List view name = 名稱 modified = 修改日期 size = 大小 # Dialogs ## Compress Dialog create-archive = 建立壓縮檔案 ## Empty Trash Dialog empty-trash = 清空垃圾桶 empty-trash-warning = 垃圾桶中的項目將被永久刪除 ## New File/Folder Dialog create-new-file = 建立新檔案 create-new-folder = 建立新資料夾 file-name = 檔案名稱 folder-name = 資料夾名稱 file-already-exists = 相同名稱的檔案已經存在 folder-already-exists = 相同名稱的資料夾已經存在 name-hidden = 以「.」開頭的名稱將會被隱藏 name-invalid = 名稱不能是「{ $filename }」 name-no-slashes = 名稱不能包含斜線 ## Open/Save Dialog cancel = 取消 create = 創作 open = 開啟 open-file = 開啟檔案 open-folder = 開啟資料夾 open-in-new-tab = 在新分頁中開啟 open-in-new-window = 在新視窗中開啟 open-item-location = 開啟項目位置 open-multiple-files = 開啟多個檔案 open-multiple-folders = 開啟多個資料夾 save = 儲存 save-file = 儲存檔案 ## Rename Dialog rename-file = 重新命名檔案 rename-folder = 重新命名資料夾 ## Replace Dialog replace = 取代 replace-title = 「{ $filename }」已存在於此位置 replace-warning = 你要取代它嗎?取代將覆蓋其內容。 replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。 original-file = 原始檔案 replace-with = 取代為 apply-to-all = 套用至全部 keep-both = 保留兩者 skip = 跳過 ## Metadata Dialog owner = 擁有者 group = 群組 other = 其他 # Context Pages ## About ## Add Network Drive add-network-drive = 新增網路磁碟機 connect = 連線 connect-anonymously = 匿名連線 connecting = 連線中... domain = 網域 enter-server-address = 輸入伺服器地址 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:// network-drive-error = 無法存取網路磁碟機 password = 密碼 remember-password = 記住密碼 try-again = 再試一次 username = 使用者名稱 ## Operations edit-history = 編輯歷史 history = 歷史紀錄 no-history = 無歷史記錄項目。 pending = 待處理 failed = 失敗 complete = 完成 compressing = 正在壓縮 { $items } { $items -> [one] 項目 *[other] 項目 } 從「{ $from }」到 「{ $to }」({ $progress })... compressed = 已壓縮 { $items } { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」 copy_noun = 複製 creating = 正在建立「{ $name }」於「{ $parent }」 created = 已建立「{ $name }」於「{ $parent }」 copying = 正在複製 { $items } { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」({ $progress })... copied = 已複製 { $items } { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」 emptying-trash = 正在清空 { trash }({ $progress })… emptied-trash = 已經清空 { trash } extracting = 正在解壓縮 { $items } 項目 { $items -> [one] 項目 *[other] 項目 }從「{ $from }」至「{ $to }」({ $progress })... extracted = 已解壓縮 { $items } 項目 { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」 moving = 正在移動 { $items } { $items -> [one] 項目 *[other] 項目 }從「{ $from }」到「{ $to }」({ $progress })... moved = 已經移動 { $items } { $items -> [one] 項目 *[other] 項目 } 從「{ $from }」至「{ $to }」 renaming = 正在重新命名「{ $from }」至「{ $to }」 renamed = 已經重新命名「{ $from }」至「{ $to }」 restoring = 正在還原 { $items } 項目 { $items -> [one] 項目 *[other] 項目 }自 { trash } ({ $progress })... restored = 已經還原 { $items } 項目 { $items -> [one] 項目 *[other] 項目 }從 { trash } unknown-folder = 不明資料夾 ## Open with menu-open-with = 開啟檔案... default-app = { $name } (預設) ## Show details show-details = 顯示詳細資料 ## Settings settings = 設定 ### Appearance appearance = 外觀 theme = 主題 match-desktop = 符合桌面 dark = 深色 light = 淺色 # Context menu add-to-sidebar = 添加至側邊欄 compress = 壓縮… extract-here = 解壓縮 new-file = 新建檔案... new-folder = 新建資料夾... open-in-terminal = 在終端機中開啟 move-to-trash = 移動至垃圾桶 restore-from-trash = 從垃圾桶還原 remove-from-sidebar = 從側邊欄移除 sort-by-name = 依名稱排序 sort-by-modified = 依修改日期排序 sort-by-size = 依大小排序 # Menu ## File file = 檔案 new-tab = 新建分頁 new-window = 新建視窗 rename = 重新命名... close-tab = 關閉分頁 quit = 退出 ## Edit edit = 編輯 cut = 剪下 copy = 複製 paste = 貼上 select-all = 全選 ## View zoom-in = 放大 default-size = 預設大小 zoom-out = 縮小 view = 檢視 grid-view = 網格檢視 list-view = 列表檢視 show-hidden-files = 顯示隱藏檔案 list-directories-first = 目錄優先列出 menu-settings = 設定... 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 = 從大到小 deleted = 已經刪除 { $items } { $items -> [one] 項目 *[other] 項目 }從 { trash } permanently-deleting = 正在永久刪除 { $items } { $items -> [one] 项目 *[other] 项目 } permanently-deleted = 已經永久刪除 { $items } { $items -> [one] 项目 *[other] 项目 } removing-from-recents = 正在從 { recents } 中移除 { $items } { $items -> [one] 项目 *[other] 项目 } deleting = 正在刪除 { $items } { $items -> [one] 项目 *[other] 项目 }從 { trash }({ $progress })… removed-from-recents = 已經從 { 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 = 混合 ================================================ FILE: i18n.toml ================================================ fallback_language = "en" [fluent] assets_dir = "i18n" ================================================ FILE: justfile ================================================ name := 'cosmic-files' export APPID := 'com.system76.CosmicFiles' rootdir := '' prefix := '/usr' base-dir := absolute_path(clean(rootdir / prefix)) export INSTALL_DIR := base-dir / 'share' cargo-target-dir := env('CARGO_TARGET_DIR', 'target') bin-src := cargo-target-dir / 'release' / name bin-dst := base-dir / 'bin' / name applet-name := name + '-applet' applet-src := cargo-target-dir / 'release' / applet-name applet-dst := base-dir / 'bin' / applet-name desktop := APPID + '.desktop' desktop-src := 'target/xdgen' / desktop desktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop metainfo := APPID + '.metainfo.xml' metainfo-src := 'target/xdgen' / metainfo metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo icons-src := 'res' / 'icons' / 'hicolor' icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor' # Default recipe which runs `just build-release` default: build-release # Runs `cargo clean` clean: cargo clean # Removes vendored dependencies clean-vendor: rm -rf .cargo vendor vendor.tar # `cargo clean` and removes vendored dependencies clean-dist: clean clean-vendor # Compiles with debug profile build-debug *args: cargo build {{args}} cargo build --package {{applet-name}} {{args}} # Compiles with release profile build-release *args: (build-debug '--release' args) # Compiles applet with release profile build-release-applet *args: cargo build --package {{applet-name}} --release {{args}} # Compiles release profile with vendored dependencies build-vendored *args: vendor-extract (build-release '--frozen --offline' args) # Runs a clippy check check *args: cargo clippy --all-features {{args}} -- -W clippy::pedantic # Runs a clippy check with JSON message format check-json: (check '--message-format=json') # Developer target dev *args: cargo fmt just run {{args}} # Run with debug logs run *args: cargo build --release env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full {{bin-src}} {{args}} # Run tests test *args: cargo test {{args}} flamegraph *args: cargo flamegraph --release --bin cosmic-files -- --no-daemon {{args}} xdg-open flamegraph.svg heaptrack *args: #!/usr/bin/env bash set -ex rm -fv heaptrack.cosmic-files.* cargo heaptrack --profile release-with-debug --bin cosmic-files -- --no-daemon {{args}} zstd -dc < heaptrack.cosmic-files.*.raw.zst | /usr/lib/heaptrack/libexec/heaptrack_interpret | zstd -c > heaptrack.cosmic-files.zst heaptrack_gui heaptrack.cosmic-files.zst # Installs files install: install -Dm0755 {{bin-src}} {{bin-dst}} install -Dm0755 {{applet-src}} {{applet-dst}} install -Dm0644 {{desktop-src}} {{desktop-dst}} install -Dm0644 {{metainfo-src}} {{metainfo-dst}} for size in `ls {{icons-src}}`; do \ install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \ done # Installs applet files install-applet: install -Dm0755 {{applet-src}} {{applet-dst}} # Uninstalls installed files uninstall: rm -f {{bin-dst}} {{applet-dst}} # Vendor dependencies locally vendor: #!/usr/bin/env bash mkdir -p .cargo cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml echo 'directory = "vendor"' >> .cargo/config.toml echo >> .cargo/config.toml echo '[env]' >> .cargo/config.toml if [ -n "${SOURCE_DATE_EPOCH}" ] then source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")" echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml fi if [ -n "${SOURCE_GIT_HASH}" ] then echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml fi tar pcf vendor.tar .cargo vendor rm -rf .cargo vendor # Extracts vendored dependencies vendor-extract: rm -rf vendor tar pxf vendor.tar ================================================ FILE: res/com.system76.CosmicFiles.desktop ================================================ [Desktop Entry] Name=COSMIC Files Comment=File manager for the COSMIC desktop Exec=cosmic-files %U Terminal=false Type=Application StartupNotify=true Icon=com.system76.CosmicFiles Categories=COSMIC;Utility;FileManager; Keywords=Folder;Manager; MimeType=inode/directory; ================================================ FILE: res/com.system76.CosmicFiles.metainfo.xml ================================================ com.system76.CosmicFiles CC0-1.0 GPL-3.0-only COSMIC System76 jeremy@system76.com https://github.com/pop-os/cosmic-files https://github.com/pop-os/cosmic-files COSMIC Files File manager for the COSMIC desktop

File manager for the COSMIC desktop

مدير ملفات لسطح مكتب COSMIC

Správce souborů pro prostředí COSMIC

Menedżer plików pulpitu COSMIC

Fájlkezelő a COSMIC asztali környezethez

Gerenciador de arquivos do COSMIC

Gerenciador de arquivos do COSMIC

Správca súborov pre prostredie COSMIC

Gestor de archivos de COSMIC

File manager di COSMIC

Filhanterare för skrivbordsmiljön COSMIC

Folder Manager com.system76.CosmicFiles.desktop https://raw.githubusercontent.com/pop-os/cosmic-files/master/res/icons/hicolor/256x256/apps/com.system76.CosmicFiles.svg com.system76.CosmicApplication inode/directory cosmic-files
================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "1.93.0" components = ["clippy", "rustfmt"] ================================================ FILE: rustfmt.toml ================================================ imports_granularity = "Module" ================================================ FILE: samples/i18n/منزل ================================================ ================================================ FILE: samples/i18n/主目錄 ================================================ ================================================ FILE: samples/mime/application/javascript.js ================================================ console.log("This is an example JavaScript source file"); ================================================ FILE: samples/mime/application/x-shellscript.sh ================================================ #!/bin/sh echo "This is an example shell file" ================================================ FILE: samples/mime/application/x-yaml.yaml ================================================ key: value ================================================ FILE: samples/mime/check.sh ================================================ #!/usr/bin/env bash set -e cd "$(dirname "$0")" for file in */* do filetype="$(xdg-mime query filetype "${file}")" if [ -z "${filetype}" ] then echo "${file}: no filetype found" exit 1 fi if [ "${file%.*}" != "${filetype}" ] then echo "${file} is not named according to filetype ${filetype}" exit 1 fi default="$(xdg-mime query default "${filetype}")" if [ -n "${default}" ] then echo "${file}: ${filetype}: ${default}" else echo "${file}: ${filetype}: no default application found" fi done ================================================ FILE: samples/mime/text/css.css ================================================ example-css-file { color: #000000; } ================================================ FILE: samples/mime/text/csv.csv ================================================ Example,CSV,file ================================================ FILE: samples/mime/text/html.html ================================================ Example HTML file ================================================ FILE: samples/mime/text/markdown.md ================================================ # Markdown This is an example markdown file ================================================ FILE: samples/mime/text/plain.txt ================================================ This is a plain text file. ================================================ FILE: samples/mime/text/rust.rs ================================================ fn main() { println!("This is an example Rust source file"); } ================================================ FILE: samples/mime/text/x-chdr.h ================================================ // This is an example C header file ================================================ FILE: samples/mime/text/x-csrc.c ================================================ #include int main(int argc, char **argv) { printf("This is an example C source file\n"); } ================================================ FILE: samples/mode/.gitignore ================================================ /0* ================================================ FILE: samples/mode/create.sh ================================================ #!/usr/bin/env bash set -ex rm -f 0* for umode in 4 5 6 7 do for gmode in 0 4 5 6 7 do for amode in 0 4 5 6 7 do mode="0${umode}${gmode}${amode}" touch "${mode}" chmod "${mode}" "${mode}" done done done ================================================ FILE: scripts/copy.sh ================================================ #!/usr/bin/env bash set -ex cargo fmt cargo build --release --example copy rm -rf test mkdir test cp -a samples test/a mkdir test/a/link touch test/a/link/a ln -s a test/a/link/b mkdir test/a/perms touch test/a/perms/400 chmod 400 test/a/perms/400 touch test/a/perms/600 chmod 600 test/a/perms/600 touch test/a/perms/700 chmod 700 test/a/perms/700 time target/release/examples/copy ls -lR test meld test/a test/c ================================================ FILE: src/app.rs ================================================ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::app::{self, Core, Task, context_drawer}; use cosmic::cosmic_config::{self, ConfigSet}; use cosmic::iced::clipboard::dnd::DndAction; use cosmic::iced::core::SmolStr; use cosmic::iced::core::widget::operation::focusable::unfocus; use cosmic::iced::futures::{self, SinkExt}; use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers}; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::platform_specific::shell::wayland::commands::overlap_notify::overlap_notify; use cosmic::iced::runtime::{clipboard, task}; use cosmic::iced::widget::button::focus; use cosmic::iced::widget::scrollable; use cosmic::iced::widget::scrollable::AbsoluteOffset; use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId}; use cosmic::iced::{ self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream, }; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::{ Limits, Point, event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, platform_specific::runtime::wayland::layer_surface::{ IcedMargin, IcedOutput, SctkLayerSurfaceSettings, }, platform_specific::shell::wayland::commands::layer_surface::{ Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface, }, }; use cosmic::widget::about::About; use cosmic::widget::dnd_destination::DragId; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::segmented_button::{self, Entity, ReorderEvent}; use cosmic::widget::{self, icon, settings, space}; use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme}; use mime_guess::Mime; use notify_debouncer_full::notify::{self, RecommendedWatcher}; use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; use rustc_hash::{FxHashMap, FxHashSet}; use slotmap::Key as SlotMapKey; use std::any::TypeId; use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::future::Future; use std::num::NonZeroU16; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::{Arc, LazyLock, Mutex}; use std::time::{self, Duration, Instant}; use std::{env, fmt, fs, io, process}; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use wayland_client::{Proxy, protocol::wl_output::WlOutput}; use crate::clipboard::{ ClipboardCache, ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage, ClipboardPasteText, ClipboardPasteVideo, }; use crate::config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, TimeConfig, TypeToSearch, }; use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}; use crate::key_bind::key_binds; use crate::localize::LANGUAGE_SORTER; use crate::mime_app::{self, MimeApp, MimeAppCache}; use crate::mounter::{ MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, }; use crate::operation::{ Controller, Operation, OperationError, OperationErrorType, OperationSelection, ReplaceResult, copy_unique_path, }; use crate::spawn_detached::spawn_detached; use crate::tab::{ self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, SearchLocation, Tab, }; use crate::trash::{Trash, TrashExt}; use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default}; use crate::{FxOrderMap, context_action, fl, home_dir, menu, mime_icon}; static PERMANENT_DELETE_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("permanent-delete-button")); static DELETE_TRASH_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("delete-trash-button")); static CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("confirm-open-with-button")); static CONFIRM_CONTEXT_ACTION_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("confirm-context-action-button")); static EMPTY_TRASH_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("empty-trash-button")); static SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("set-executable-and-launch-confirm-button")); static FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("favorite-path-error-remove-button")); static MOUNT_ERROR_TRY_AGAIN_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("mount-error-try-again-button")); pub(crate) static REPLACE_BUTTON_ID: LazyLock = LazyLock::new(|| widget::Id::new("replace-button")); #[derive(Clone, Debug)] pub enum Mode { App, Desktop, } #[derive(Clone, Debug)] pub struct Flags { pub config_handler: Option, pub config: Config, pub state_handler: Option, pub state: State, pub mode: Mode, pub locations: Vec, pub uris: Vec, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { About, AddToSidebar, Compress, Copy, CopyPath, CopyTo, Cut, CosmicSettingsDesktop, CosmicSettingsDisplays, CosmicSettingsWallpaper, DesktopViewOptions, Delete, EditHistory, EditLocation, Eject, EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(usize), ExtractHere, ExtractTo, Gallery, HistoryNext, HistoryPrevious, ItemDown, ItemLeft, ItemRight, ItemUp, LocationUp, MoveTo, NewFile, NewFolder, Open, OpenInNewTab, OpenInNewWindow, OpenItemLocation, OpenTerminal, OpenWith, RunContextAction(usize), Paste, PermanentlyDelete, Preview, Reload, RemoveFromRecents, Rename, RestoreFromTrash, SearchActivate, SelectFirst, SelectLast, SelectAll, SetSort(HeadingOptions, bool), Settings, TabClose, TabNew, TabNext, TabPrev, TabViewGrid, TabViewList, ToggleFoldersFirst, ToggleShowHidden, ToggleSort(HeadingOptions), WindowClose, WindowNew, ZoomDefault, ZoomIn, ZoomOut, Recents, } impl Action { const fn message(&self, entity_opt: Option) -> Message { match self { Self::About => Message::ToggleContextPage(ContextPage::About), Self::AddToSidebar => Message::AddToSidebar(entity_opt), Self::Compress => Message::Compress(entity_opt), Self::Copy => Message::Copy(entity_opt), Self::CopyPath => Message::CopyPath(entity_opt), Self::CopyTo => Message::CopyTo(entity_opt), Self::Cut => Message::Cut(entity_opt), Self::CosmicSettingsDesktop => Message::CosmicSettings("desktop"), Self::CosmicSettingsDisplays => Message::CosmicSettings("displays"), Self::CosmicSettingsWallpaper => Message::CosmicSettings("wallpaper"), Self::Delete => Message::Delete(entity_opt), Self::DesktopViewOptions => Message::DesktopViewOptions, Self::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory), Self::EditLocation => Message::TabMessage(entity_opt, tab::Message::EditLocationEnable), Self::Eject => Message::Eject, Self::EmptyTrash => Message::TabMessage(None, tab::Message::EmptyTrash), Self::ExtractHere => Message::ExtractHere(entity_opt), Self::ExtractTo => Message::ExtractTo(entity_opt), #[cfg(feature = "desktop")] Self::ExecEntryAction(action) => { Message::TabMessage(entity_opt, tab::Message::ExecEntryAction(None, *action)) } Self::Gallery => Message::TabMessage(entity_opt, tab::Message::GalleryToggle), Self::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext), Self::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious), Self::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown), Self::ItemLeft => Message::TabMessage(entity_opt, tab::Message::ItemLeft), Self::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight), Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp), Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp), Self::MoveTo => Message::MoveTo(entity_opt), Self::NewFile => Message::NewItem(entity_opt, false), Self::NewFolder => Message::NewItem(entity_opt, true), Self::Open => Message::TabMessage(entity_opt, tab::Message::Open(None)), Self::OpenInNewTab => Message::OpenInNewTab(entity_opt), Self::OpenInNewWindow => Message::OpenInNewWindow(entity_opt), Self::OpenItemLocation => Message::OpenItemLocation(entity_opt), Self::OpenTerminal => Message::OpenTerminal(entity_opt), Self::OpenWith => Message::OpenWithDialog(entity_opt), Self::RunContextAction(action) => { Message::TabMessage(entity_opt, tab::Message::RunContextAction(*action)) } Self::Paste => Message::Paste(entity_opt), Self::PermanentlyDelete => Message::PermanentlyDelete(entity_opt), Self::Preview => Message::Preview(entity_opt), Self::Reload => Message::TabMessage(entity_opt, tab::Message::Reload), Self::RemoveFromRecents => Message::RemoveFromRecents(entity_opt), Self::Rename => Message::Rename(entity_opt), Self::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Self::SearchActivate => Message::SearchActivate, Self::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll), Self::SelectFirst => Message::TabMessage(entity_opt, tab::Message::SelectFirst), Self::SelectLast => Message::TabMessage(entity_opt, tab::Message::SelectLast), Self::SetSort(sort, dir) => { Message::TabMessage(entity_opt, tab::Message::SetSort(*sort, *dir)) } Self::Settings => Message::ToggleContextPage(ContextPage::Settings), Self::TabClose => Message::TabClose(entity_opt), Self::TabNew => Message::TabNew, Self::TabNext => Message::TabNext, Self::TabPrev => Message::TabPrev, Self::TabViewGrid => Message::TabView(entity_opt, tab::View::Grid), Self::TabViewList => Message::TabView(entity_opt, tab::View::List), Self::ToggleFoldersFirst => Message::ToggleFoldersFirst, Self::ToggleShowHidden => Message::ToggleShowHidden, Self::ToggleSort(sort) => { Message::TabMessage(entity_opt, tab::Message::ToggleSort(*sort)) } Self::WindowClose => Message::WindowClose, Self::WindowNew => Message::WindowNew, Self::ZoomDefault => Message::ZoomDefault(entity_opt), Self::ZoomIn => Message::ZoomIn(entity_opt), Self::ZoomOut => Message::ZoomOut(entity_opt), Self::Recents => Message::Recents, } } } impl MenuAction for Action { type Message = Message; fn message(&self) -> Message { self.message(None) } } #[derive(Clone, Debug)] pub struct PreviewItem(pub Box); impl PartialEq for PreviewItem { fn eq(&self, other: &Self) -> bool { self.0.location_opt == other.0.location_opt } } impl Eq for PreviewItem {} #[derive(Clone, Debug, Eq, PartialEq)] pub enum PreviewKind { Custom(PreviewItem), Location(Location), Selected, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum NavMenuAction { ClearRecents, EmptyTrash, Open(segmented_button::Entity), OpenWith(segmented_button::Entity), OpenInNewTab(segmented_button::Entity), OpenInNewWindow(segmented_button::Entity), Preview(segmented_button::Entity), RunContextAction(segmented_button::Entity, usize), RemoveFromSidebar(segmented_button::Entity), } impl MenuAction for NavMenuAction { type Message = cosmic::Action; fn message(&self) -> Self::Message { cosmic::Action::App(Message::NavMenuAction(*self)) } } /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { AddToSidebar(Option), AppTheme(AppTheme), CloseToast(widget::ToastId), Compress(Option), Config(Config), Copy(Option), CopyPath(Option), CopyTo(Option), CopyToResult(DialogResult), CosmicSettings(&'static str), Cut(Option), Delete(Option), DesktopConfig(DesktopConfig), DesktopViewOptions, DesktopDialogs(bool), DialogCancel, DialogComplete, Eject, FileDialogMessage(DialogMessage), DialogPush(DialogPage, Option), DialogUpdate(DialogPage), DialogUpdateComplete(DialogPage), ExtractHere(Option), ExtractTo(Option), ExtractToResult(DialogResult), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Focused(window::Id), Key(window::Id, Modifiers, Key, Option), LaunchUrl(String), MaybeExit, ModifiersChanged(window::Id, Modifiers), MounterItems(MounterKey, MounterItems), MountResult(MounterKey, MounterItem, Result), Mouse(window::Id, mouse::Button), MoveTo(Option), MoveToResult(DialogResult), NavBarClose(Entity), NavBarContext(Entity), NavMenuAction(NavMenuAction), NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender), NetworkDriveInput(String), NetworkDriveOpenEntityAfterMount { entity: Entity, }, NetworkDriveOpenTabAfterMount { location: Location, }, NetworkDriveSubmit, NetworkResult(MounterKey, String, Result), NewItem(Option, bool), #[cfg(feature = "notify")] Notification(Arc>), NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), OpenTerminal(Option), OpenInNewTab(Option), OpenInNewWindow(Option), OpenItemLocation(Option), OpenWithBrowse, OpenWithDialog(Option), OpenWithSelection(usize), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Overlap(window::Id, OverlapNotifyEvent), Paste(Option), PasteContents(PathBuf, ClipboardPaste), PasteImage(PathBuf), PasteImageContents(PathBuf, ClipboardPasteImage), PasteText(PathBuf), PasteTextContents(PathBuf, ClipboardPasteText), PasteVideo(PathBuf), PasteVideoContents(PathBuf, ClipboardPasteVideo), CheckClipboard, CheckClipboardImage, CheckClipboardVideo, CheckClipboardText, RetryCheckClipboard(ClipboardCache), ClipboardCached(ClipboardCache), PendingCancel(u64), PendingCancelAll, PendingComplete(u64, OperationSelection), PendingDismiss, PendingError(u64, OperationError), PendingResults(Vec<(u64, OperationSelection)>, Vec<(u64, OperationError)>), PendingPause(u64, bool), PendingPauseAll(bool), PermanentlyDelete(Option), Preview(Option), ReorderTab(ReorderEvent), RescanRecents, RescanTrash, RemoveFromRecents(Option), Rename(Option), ReplaceResult(ReplaceResult), RestoreFromTrash(Option), SaveSortNames, ScrollTab(i16), SearchActivate, SearchClear, SearchInput(String), SetShowDetails(bool), SetShowRecents(bool), SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), TabActivate(Entity), TabNext, TabPrev, TabClose(Option), TabConfig(TabConfig), TabMessage(Option, tab::Message), TabNew, TabRescan( Entity, Location, Option>, Vec, Option>, ), TabView(Option, tab::View), TimeConfigChange(TimeConfig), ToggleContextPage(ContextPage), ToggleFoldersFirst, ToggleShowHidden, Undo(usize), UndoTrash(widget::ToastId, Arc<[PathBuf]>), UndoTrashStart(Vec), WindowClose, WindowCloseRequested(window::Id), WindowMaximize(window::Id, bool), WindowNew, ZoomDefault(Option), ZoomIn(Option), ZoomOut(Option), DndHoverLocTimeout(Location), DndHoverTabTimeout(Entity), DndEnterNav(Entity), DndExitNav, DndEnterTab(Entity, Vec), DndExitTab, DndDropTab(Entity, Option, DndAction), DndDropNav(Entity, Option, DndAction), Recents, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] OutputEvent(OutputEvent, WlOutput), Cosmic(app::Action), None, Surface(surface::Action), CutPaths(Vec), } #[derive(Clone, Debug, Eq, PartialEq)] pub enum ContextPage { About, EditHistory, NetworkDrive, Preview(Option, PreviewKind), Settings, } #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum ArchiveType { Tgz, #[default] Zip, } impl ArchiveType { pub const fn all() -> &'static [Self] { &[Self::Tgz, Self::Zip] } pub const fn extension(&self) -> &str { match self { Self::Tgz => ".tgz", Self::Zip => ".zip", } } } impl AsRef for ArchiveType { fn as_ref(&self) -> &str { self.extension() } } #[derive(Clone, Debug)] pub enum DialogPage { Compress { paths: Box<[PathBuf]>, to: PathBuf, name: String, archive_type: ArchiveType, password: Option, }, EmptyTrash, FailedOperation(u64), FailedOperations(Vec), ExtractPassword { id: u64, password: String, }, MountError { mounter_key: MounterKey, item: MounterItem, error: String, }, NetworkAuth { mounter_key: MounterKey, uri: String, auth: MounterAuth, auth_tx: mpsc::Sender, }, NetworkError { mounter_key: MounterKey, uri: String, error: String, }, NewItem { parent: PathBuf, name: String, dir: bool, }, RunContextAction { action: usize, paths: Box<[PathBuf]>, }, OpenWith { path: PathBuf, mime: mime_guess::Mime, selected: usize, store_opt: Option, }, PermanentlyDelete { paths: Box<[PathBuf]>, }, DeleteTrash { items: Vec, }, RenameItem { from: PathBuf, parent: PathBuf, name: String, dir: bool, }, Replace { from: Box, to: Box, multiple: bool, apply_to_all: bool, conflict_count: usize, tx: mpsc::Sender, }, SetExecutableAndLaunch { path: PathBuf, }, FavoritePathError { path: PathBuf, entity: Entity, }, } pub struct DialogPages { pages: VecDeque, } impl Default for DialogPages { fn default() -> Self { Self::new() } } impl DialogPages { pub const fn new() -> Self { Self { pages: VecDeque::new(), } } pub fn front(&self) -> Option<&DialogPage> { self.pages.front() } pub fn front_mut(&mut self) -> Option<&mut DialogPage> { self.pages.front_mut() } pub fn push_back(&mut self, page: DialogPage) -> Task { let task = if self.pages.is_empty() { Task::done(cosmic::Action::App(Message::DesktopDialogs(true))) } else { Task::none() }; self.pages.push_back(page); task } pub fn push_front(&mut self, page: DialogPage) -> Task { let task = if self.pages.is_empty() { Task::done(cosmic::Action::App(Message::DesktopDialogs(true))) } else { Task::none() }; self.pages.push_front(page); task } #[must_use] pub fn pop_front(&mut self) -> Option<(DialogPage, Task)> { let page = self.pages.pop_front()?; let task = if self.pages.is_empty() { Task::done(cosmic::Action::App(Message::DesktopDialogs(false))) } else { Task::none() }; Some((page, task)) } pub fn update_front(&mut self, page: DialogPage) { if !self.pages.is_empty() { self.pages[0] = page; } } } pub struct FavoriteIndex(usize); #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum MimeAppMatch { Exact, Related, Other, } pub struct MounterData(MounterKey, MounterItem); #[derive(Clone, Debug)] pub enum WindowKind { ContextMenu(Entity, widget::Id), Desktop(Entity), DesktopViewOptions, Dialogs(widget::Id), FileDialog(Option>), Preview(Option, PreviewKind), } pub struct WatcherWrapper { watcher_opt: Option>, } impl Clone for WatcherWrapper { fn clone(&self) -> Self { Self { watcher_opt: None } } } impl fmt::Debug for WatcherWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WatcherWrapper").finish() } } impl PartialEq for WatcherWrapper { fn eq(&self, _other: &Self) -> bool { false } } struct Window { kind: WindowKind, modifiers: Modifiers, } impl Window { fn new(kind: WindowKind) -> Self { Self { kind, modifiers: Modifiers::empty(), } } } // The [`App`] stores application-specific state. pub struct App { core: Core, about: About, nav_bar_context_id: segmented_button::Entity, nav_model: segmented_button::SingleSelectModel, tab_model: segmented_button::Model, config_handler: Option, state_handler: Option, config: Config, state: State, mode: Mode, app_themes: Vec, compio_tx: mpsc::Sender + Send>>>, context_page: ContextPage, dialog_pages: DialogPages, dialog_text_input: widget::Id, key_binds: HashMap, margin: FxHashMap, mime_app_cache: MimeAppCache, modifiers: Modifiers, mounter_items: FxHashMap, must_save_sort_names: bool, network_drive_connecting: Option<(MounterKey, String)>, network_drive_input: String, #[cfg(feature = "notify")] notification_opt: Option>>, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] overlap: FxHashMap, pending_operation_id: u64, pending_operations: BTreeMap, progress_operations: BTreeSet, complete_operations: BTreeMap, failed_operations: BTreeMap, scrollable_id: widget::Id, search_id: widget::Id, size: Option, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] layer_sizes: FxHashMap, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] surface_ids: FxHashMap, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] surface_names: FxHashMap, toasts: widget::toaster::Toasts, watcher_opt: Option<( Debouncer, FxHashSet, )>, windows: FxHashMap, nav_dnd_hover: Option<(Location, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>, type_select_prefix: String, type_select_last_key: Option, nav_drag_id: DragId, tab_drag_id: DragId, auto_scroll_speed: Option, file_dialog_opt: Option>, clipboard_cache: ClipboardCache, } impl App { /// Returns true if the clipboard cache contains pasteable content fn clipboard_has_content(&self) -> bool { !matches!(self.clipboard_cache, ClipboardCache::Empty) } fn push_dialog(&mut self, page: DialogPage, focus_id: Option) -> Task { let t = self.dialog_pages.push_back(page); if let Some(focus_id) = focus_id { Task::batch([t, focus(focus_id)]) } else { t } } fn open_file(&mut self, paths: &[impl AsRef]) -> Task { let mut tasks = Vec::new(); // Associate all paths to its MIME type // This allows handling paths as groups if possible, such as launching a single video // player that is passed every path. let mut groups: FxHashMap> = FxHashMap::default(); let mut all_archives = true; let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; for (mime, path) in paths.iter().map(|path| { ( mime_icon::mime_for_path(path, None, false), path.as_ref().to_owned(), ) }) { if all_archives && !supported_archive_types.iter().copied().any(|t| mime == t) { all_archives = false; } groups.entry(mime).or_default().push(path); } if all_archives { // Use extract to dialog if all selected paths are supported archives return self.extract_to(paths); } 'outer: for (mime, paths) in groups { log::debug!("Attempting to launch app\n\tfor: {mime}\n\twith: {paths:?}"); // First launch apps that can be launched directly if mime == "application/x-desktop" { #[cfg(feature = "desktop")] { // Try opening desktop application Self::launch_desktop_entries(&paths); continue; } } else if mime == "application/x-executable" || mime == "application/vnd.appimage" { // Try opening executable for path in paths { let mut command = std::process::Command::new(&path); match spawn_detached(&mut command) { Ok(()) => {} Err(err) => match err.kind() { io::ErrorKind::PermissionDenied => { // If permission is denied, try marking as executable, then running tasks.push(self.push_dialog( DialogPage::SetExecutableAndLaunch { path }, Some(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()), )); } _ => { log::warn!("failed to execute {}: {}", path.display(), err); } }, } } continue; } // Try mime apps, which should be faster than xdg-open if self.launch_from_mime_cache(&mime, &paths) { continue; } // loop through subclasses if available if let Some(mime_sub_classes) = mime_icon::parent_mime_types(&mime) { for sub_class in mime_sub_classes { if self.launch_from_mime_cache(&sub_class, &paths) { continue 'outer; } } } // Fall back to using open crate for path in paths { match open::that_detached(&path) { Ok(()) => { if self.config.show_recents { let _ = recently_used_xbel::update_recently_used( &path, Self::APP_ID.to_string(), "cosmic-files".to_string(), None, ); } } Err(err) => { log::warn!("failed to open {}: {}", path.display(), err); } } } } Task::batch(tasks) } #[cfg(feature = "desktop")] fn launch_desktop_entries(paths: &[impl AsRef]) { use cosmic::desktop::fde::DesktopEntry; let locales = cosmic::desktop::fde::get_languages_from_env(); for path in paths.iter().map(AsRef::as_ref) { match DesktopEntry::from_path::<&str>(path, None) { Ok(entry) => match entry.exec() { Some(exec) => { match mime_app::exec_to_command( exec, entry.name(&locales).as_deref().unwrap_or_default(), Some(path), &[] as &[&str; 0], ) { Some(commands) => { let cwd_opt = entry.desktop_entry("Path"); for mut command in commands { if let Some(cwd) = cwd_opt { command.current_dir(cwd); } if let Err(err) = spawn_detached(&mut command) { log::warn!("failed to execute {}: {}", path.display(), err); } } } None => { log::warn!( "failed to parse {}: invalid Desktop Entry/Exec", path.display() ); } } } None => { log::warn!( "failed to parse {}: missing Desktop Entry/Exec", path.display() ); } }, Err(err) => { log::warn!("failed to parse {}: {}", path.display(), err); } } } } fn launch_from_mime_cache

(&self, mime: &Mime, paths: &[P]) -> bool where P: std::fmt::Debug + AsRef + AsRef, { for app in self.mime_app_cache.get(mime) { let Some(commands) = app.command(paths) else { continue; }; let len = commands.len(); for (i, mut command) in commands.into_iter().enumerate() { match spawn_detached(&mut command) { Ok(()) => { if self.config.show_recents { for path in paths { let _ = recently_used_xbel::update_recently_used( &path.into(), Self::APP_ID.to_string(), "cosmic-files".to_string(), None, ); } } return true; } Err(err) => { // More than one command: The app doesn't support lists of paths so each command // is associated with one instance // // One command: Attempted to launch one app with multiple paths let path = if len > 1 { format!("{:?}", paths.get(i)) } else { format!("{paths:?}") }; log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err); } } } } // No app matched for mimes and paths false } #[cfg(feature = "desktop")] fn exec_entry_action(entry: &cosmic::desktop::DesktopEntryData, action: usize) { if let Some(action) = entry.desktop_actions.get(action) { // Largely copied from COSMIC app library let mut exec = shlex::Shlex::new(&action.exec); match exec.next() { Some(cmd) if !cmd.contains('=') => { let mut proc = tokio::process::Command::new(cmd); proc.args(exec.filter(|arg| !arg.starts_with('%'))); let _ = proc.spawn(); } _ => (), } } else { log::warn!( "Invalid actions index `{action}` for desktop entry {}", entry.name ); } } fn destination_selection_dialog( &mut self, paths: &[impl AsRef], on_result: impl Fn(DialogResult) -> Message + 'static, title: impl Into, accept_label: impl AsRef, ) -> Task { if let Some(destination) = paths .first() .and_then(|first| first.as_ref().parent()) .map(Path::to_path_buf) { let (mut dialog, dialog_task) = Dialog::new( DialogSettings::new() .kind(DialogKind::OpenFolder) .path(destination), Message::FileDialogMessage, on_result, ); let set_title_task = dialog.set_title(title); dialog.set_accept_label(accept_label); self.windows.insert( dialog.window_id(), Window::new(WindowKind::FileDialog(Some( paths.iter().map(|x| x.as_ref().to_path_buf()).collect(), ))), ); self.file_dialog_opt = Some(dialog); Task::batch([set_title_task, dialog_task]) } else { Task::none() } } fn extract_to(&mut self, paths: &[impl AsRef]) -> Task { self.destination_selection_dialog( paths, Message::ExtractToResult, fl!("extract-to-title"), fl!("extract-here"), ) } fn move_to(&mut self, paths: &[impl AsRef]) -> Task { self.destination_selection_dialog( paths, Message::MoveToResult, fl!("move-to-title"), fl!("move-to-button-label"), ) } fn copy_to(&mut self, paths: &[impl AsRef]) -> Task { self.destination_selection_dialog( paths, Message::CopyToResult, fl!("copy-to-title"), fl!("copy-to-button-label"), ) } #[cfg(all(feature = "wayland", feature = "desktop-applet"))] fn handle_overlap(&mut self) { let mut overlaps: FxHashMap<_, _> = self .windows .keys() .map(|k| (*k, (0., 0., 0., 0.))) .collect(); let mut sorted_overlaps: Box<[_]> = self.overlap.values().collect(); sorted_overlaps .sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height))); for (w_id, overlap) in sorted_overlaps { let Some((bl, br, tl, tr, mut size)) = self.layer_sizes.get(w_id).map(|s| { ( Rectangle::new( Point::new(0., s.height / 2.), Size::new(s.width / 2., s.height / 2.), ), Rectangle::new( Point::new(s.width / 2., s.height / 2.), Size::new(s.width / 2., s.height / 2.), ), Rectangle::new(Point::new(0., 0.), Size::new(s.width / 2., s.height / 2.)), Rectangle::new( Point::new(s.width / 2., 0.), Size::new(s.width / 2., s.height / 2.), ), *s, ) }) else { continue; }; let tl = tl.intersects(overlap); let tr = tr.intersects(overlap); let bl = bl.intersects(overlap); let br = br.intersects(overlap); let Some((top, left, bottom, right)) = overlaps.get_mut(w_id) else { continue; }; if tl && tr { *top += overlap.height; } if tl && bl { *left += overlap.width; } if bl && br { *bottom += overlap.height; } if tr && br { *right += overlap.width; } let min_dim = if overlap.width / size.width.max(1.) > overlap.height / size.height.max(1.) { (0., overlap.height) } else { (overlap.width, 0.) }; // just one quadrant with overlap if tl && !(tr || bl) { *top += min_dim.1; *left += min_dim.0; size.height -= min_dim.1; size.width -= min_dim.0; } if tr && !(tl || br) { *top += min_dim.1; *right += min_dim.0; size.height -= min_dim.1; size.width -= min_dim.0; } if bl && !(br || tl) { *bottom += min_dim.1; *left += min_dim.0; size.height -= min_dim.1; size.width -= min_dim.0; } if br && !(bl || tr) { *bottom += min_dim.1; *right += min_dim.0; size.height -= min_dim.1; size.width -= min_dim.0; } } self.margin = overlaps; } fn open_tab_entity( &mut self, location: Location, activate: bool, selection_paths: Option>, scrollable_id: widget::Id, window_id: Option, ) -> (Entity, Task) { let mut tab = Tab::new( location.clone(), self.config.tab, self.config.thumb_cfg, Some(&self.state.sort_names), scrollable_id, window_id, ); tab.mode = match self.mode { Mode::App => tab::Mode::App, Mode::Desktop => { tab.config.view = tab::View::Grid; tab::Mode::Desktop } }; let entity = self .tab_model .insert() .text(tab.title()) .data(tab) .closable(); let entity = if activate { entity.activate().id() } else { entity.id() }; let mut tasks = Vec::with_capacity(4); if activate { tasks.push(task::widget(unfocus())); } tasks.push(self.update_title()); tasks.push(self.update_watcher()); tasks.push(self.update_tab(entity, location, selection_paths)); (entity, Task::batch(tasks)) } fn open_tab( &mut self, location: Location, activate: bool, selection_paths: Option>, ) -> Task { self.open_tab_entity( location, activate, selection_paths, self.scrollable_id.clone(), None, ) .1 } // This wrapper ensures that local folders use trash and remote folders permanently delete with a dialog fn delete(&mut self, paths: impl IntoIterator) -> Task { let mut dialog_paths = Vec::new(); let mut trash_paths = Vec::new(); for path in paths { //TODO: is there a smarter way to check this? (like checking for trash folders) let can_trash = match path.metadata() { Ok(metadata) => matches!(tab::fs_kind(&metadata), tab::FsKind::Local), Err(err) => { log::warn!("failed to get metadata for {}: {}", path.display(), err); false } }; if can_trash { trash_paths.push(path); } else { dialog_paths.push(path); } } let mut tasks = Vec::new(); if !dialog_paths.is_empty() { tasks.push(self.update(Message::DialogPush( DialogPage::PermanentlyDelete { paths: dialog_paths.into_boxed_slice(), }, Some(PERMANENT_DELETE_BUTTON_ID.clone()), ))); } if !trash_paths.is_empty() { tasks.push(self.operation(Operation::Delete { paths: trash_paths })); } Task::batch(tasks) } fn operation(&mut self, operation: Operation) -> Task { let id = self.pending_operation_id; let controller = Controller::default(); let compio_tx = self.compio_tx.clone(); self.pending_operation_id += 1; if operation.show_progress_notification() { self.progress_operations.insert(id); } self.pending_operations .insert(id, (operation.clone(), controller.clone())); // Use a task to send operations to the compio runtime thread. cosmic::Task::stream(cosmic::iced::stream::channel(4, move |msg_tx| async move { let (tx, rx) = tokio::sync::oneshot::channel(); let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); let msg_tx_clone = msg_tx.clone(); _ = compio_tx .send(Box::pin(async move { let msg = match operation.perform(&msg_tx_clone, controller).await { Ok(result_paths) => Message::PendingComplete(id, result_paths), Err(err) => Message::PendingError(id, err), }; _ = tx.send(msg); })) .await; if let Ok(msg) = rx.await { let _ = msg_tx.lock().await.send(msg).await; } })) .map(cosmic::Action::App) } /// Will join operations together into a single task that will return a single /// Message::PendingResults message when all operations are complete. fn join_operations(&mut self, operations: Vec) -> Task { Task::batch( operations .into_iter() .map(|operation| self.operation(operation)), ) .collect() .map(|messages| { let results = messages.into_iter().fold( Message::PendingResults(Vec::new(), Vec::new()), |mut acc, message| { if let Message::PendingResults(completed, errors) = &mut acc { match message { cosmic::Action::App(Message::PendingComplete(id, selection)) => { completed.push((id, selection)); } cosmic::Action::App(Message::PendingError(id, err)) => { errors.push((id, err)); } _ => {} } } acc }, ); cosmic::Action::App(results) }) } fn handle_completed_operations( &mut self, completed: Vec<(u64, OperationSelection)>, ) -> Task { let mut commands = Vec::with_capacity(4 * completed.len()); let mut op_sel = OperationSelection::default(); for (id, op_sel_pending) in completed { op_sel.ignored.extend(op_sel_pending.ignored); op_sel.selected.extend(op_sel_pending.selected); if let Some((op, _)) = self.pending_operations.remove(&id) { // Show toast for some operations if let Some(description) = op.toast() { if let Operation::Delete { ref paths } = op { let paths: Arc<[PathBuf]> = Arc::from(paths.as_slice()); commands.push( self.toasts .push( widget::toaster::Toast::new(description) .action(fl!("undo"), move |tid| { Message::UndoTrash(tid, paths.clone()) }), ) .map(cosmic::Action::App), ); } else { commands.push( self.toasts .push(widget::toaster::Toast::new(description)) .map(cosmic::Action::App), ); } } // If a favorite for a path has been renamed or moved, update it. if let Operation::Rename { ref from, ref to } = op { if self.update_favorites([(from, to)].as_slice()) { commands.push(self.update_config()); } } else if let Operation::Move { ref paths, ref to, .. } = op { let path_changes: Box<[_]> = paths .iter() .filter_map(|from| from.file_name().map(|name| (from, to.join(name)))) .collect(); if self.update_favorites(&path_changes) { commands.push(self.update_config()); } } if matches!(op, Operation::RemoveFromRecents { .. }) { commands.push(self.rescan_recents()); } self.complete_operations.insert(id, op); } } // Close progress notification if all relevant operations are finished if !self .pending_operations .values() .any(|(op, _)| op.show_progress_notification()) { self.progress_operations.clear(); } // Potentially show a notification commands.push(self.update_notification()); // Rescan and select based on operation commands.push(self.rescan_operation_selection(op_sel)); // Manually rescan any trash tabs after any operation is completed commands.push(self.rescan_trash()); Task::batch(commands) } fn handle_operation_errors(&mut self, errors: Vec<(u64, OperationError)>) -> Task { let mut tasks = Vec::new(); let mut failed = Vec::new(); for (id, err) in errors.into_iter() { if let Some((op, controller)) = self.pending_operations.remove(&id) { // Only show dialog if not cancelled if !controller.is_cancelled() { match err.kind { OperationErrorType::Generic(_) => failed.push(id), OperationErrorType::PasswordRequired => { tasks.push(self.dialog_pages.push_back(DialogPage::ExtractPassword { id, password: String::new(), })); } } } // Remove from progress self.progress_operations.remove(&id); self.failed_operations .insert(id, (op, controller, err.to_string())); } } if !failed.is_empty() { tasks.push( self.dialog_pages .push_back(DialogPage::FailedOperations(failed)), ); tasks.push(widget::text_input::focus(self.dialog_text_input.clone())); } // Close progress notification if all relevant operations are finished if !self .pending_operations .values() .any(|(op, _)| op.show_progress_notification()) { self.progress_operations.clear(); } // Manually rescan any trash tabs after any operation is completed tasks.push(self.rescan_trash()); Task::batch(tasks) } fn remove_window(&mut self, id: &window::Id) { if let Some(window) = self.windows.remove(id) { match window.kind { WindowKind::ContextMenu(entity, _) => { // Close context menu if let Some(tab) = self.tab_model.data_mut::(entity) { tab.context_menu = None; } } WindowKind::Desktop(entity) => { // Remove the tab from the tab model self.tab_model.remove(entity); } _ => {} } } } fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task { log::info!("rescan_operation_selection {op_sel:?}"); let entity = self.tab_model.active(); let Some(tab) = self.tab_model.data::(entity) else { return Task::none(); }; let Some(items) = tab.items_opt() else { return Task::none(); }; for item in items { if item.selected { if let Some(path) = item.path_opt() && (op_sel.selected.contains(path) || op_sel.ignored.contains(path)) { // Ignore if path in selected or ignored paths continue; } // Return if there is a previous selection not matching return Task::none(); } } self.update_tab(entity, tab.location.clone(), Some(op_sel.selected)) } fn update_tab( &mut self, entity: Entity, location: Location, selection_paths: Option>, ) -> Task { if let Location::Search(_, term, ..) = location { self.search_set(entity, Some(term), selection_paths) } else { self.rescan_tab(entity, location, selection_paths) } } fn rescan_tab( &mut self, entity: Entity, location: Location, selection_paths: Option>, ) -> Task { log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}"); let icon_sizes = self.config.tab.icon_sizes; 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)) => { #[cfg(feature = "gvfs")] { let mounter_paths: Box<[_]> = mounter_items .values() .flatten() .filter_map(MounterItem::path) .collect(); if !mounter_paths.is_empty() { for item in &mut items { item.is_mount_point = item.path_opt().is_some_and(|p| mounter_paths.contains(p)); } } } cosmic::action::app(Message::TabRescan( entity, location, parent_item_opt, items, selection_paths, )) } Err(err) => { log::warn!("failed to rescan: {err}"); cosmic::action::none() } } }) } fn rescan_trash(&mut self) -> Task { let needs_reload: Box<[_]> = self .tab_model .iter() .filter_map(|entity| { let tab = self.tab_model.data::(entity)?; tab.location .is_trash() .then_some((entity, tab.location.clone())) }) .collect(); let commands = needs_reload .into_iter() .map(|(entity, location)| self.update_tab(entity, location, None)); Task::batch(commands) } fn rescan_recents(&mut self) -> Task { let needs_reload: Box<[_]> = self .tab_model .iter() .filter_map(|entity| { let tab = self.tab_model.data::(entity)?; tab.location .is_recents() .then_some((entity, tab.location.clone())) }) .collect(); let commands = needs_reload .into_iter() .map(|(entity, location)| self.update_tab(entity, location, None)); Task::batch(commands) } fn search_get(&self) -> Option<&str> { let entity = self.tab_model.active(); let tab = self.tab_model.data::(entity)?; match &tab.location { Location::Search(_, term, ..) => Some(term), _ => None, } } fn search_set_active(&mut self, term_opt: Option) -> Task { let entity = self.tab_model.active(); self.search_set(entity, term_opt, None) } fn search_set( &mut self, tab: Entity, term_opt: Option, selection_paths: Option>, ) -> Task { let mut title_location_opt = None; if let Some(tab) = self.tab_model.data_mut::(tab) { let location_opt = match term_opt { Some(term) => { let search_location = if let Some(path) = tab.location.path_opt() { Some(SearchLocation::Path(path.clone())) } else if tab.location.is_recents() { Some(SearchLocation::Recents) } else if tab.location.is_trash() { Some(SearchLocation::Trash) } else { None }; search_location.map(|search_location| { ( Location::Search( search_location, term, tab.config.show_hidden, Instant::now(), ), true, ) }) } None => match &tab.location { Location::Search(search_location, ..) => match search_location { SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)), SearchLocation::Recents => Some((Location::Recents, false)), SearchLocation::Trash => Some((Location::Trash, false)), }, _ => None, }, }; if let Some((location, focus_search)) = location_opt { tab.change_location(&location, None); title_location_opt = Some((tab.title(), tab.location.clone(), focus_search)); } } if let Some((title, location, focus_search)) = title_location_opt { self.tab_model.text_set(tab, title); return Task::batch([ self.update_title(), self.update_watcher(), self.rescan_tab(tab, location, selection_paths), if focus_search { widget::text_input::focus(self.search_id.clone()) } else { Task::none() }, ]); } Task::none() } fn selected_paths( &self, entity_opt: Option, ) -> impl Iterator + use<'_> { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); self.tab_model .data::(entity) .into_iter() .flat_map(|tab| { tab.selected_locations() .into_iter() .filter_map(Location::into_path_opt) }) } fn set_cut(&mut self, entity_opt: Option) { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { tab.cut_selected(); } } fn update_config(&mut self) -> Task { self.update_nav_model(); // Tabs are collected first to placate the borrowck let tabs: Box<[_]> = self.tab_model.iter().collect(); // Update main conf and each tab with the new config let commands = std::iter::once(cosmic::command::set_theme(self.config.app_theme.theme())) .chain(tabs.into_iter().map(|entity| { self.update(Message::TabMessage( Some(entity), tab::Message::Config(self.config.tab), )) })); Task::batch(commands) } fn update_desktop(&mut self) -> Task { let needs_reload: Box<[_]> = (self.tab_model.iter()) .filter_map(|entity| { let tab = self.tab_model.data::(entity)?; if let Location::Desktop(path, output, _) = &tab.location { Some(( entity, Location::Desktop(path.clone(), output.clone(), self.config.desktop), )) } else { None } }) .collect(); let mut commands = Vec::with_capacity(needs_reload.len()); for (entity, location) in needs_reload { if let Some(tab) = self.tab_model.data_mut::(entity) { tab.location = location.clone(); } commands.push(self.update_tab(entity, location, None)); } Task::batch(commands) } fn activate_nav_model_location(&mut self, location: &Location) { let nav_bar_id = self.nav_model.iter().find(|&id| { self.nav_model .data::(id) .is_some_and(|l| l == location) }); if let Some(id) = nav_bar_id { self.nav_model.activate(id); } else { let active = self.nav_model.active(); segmented_button::Selectable::deactivate(&mut self.nav_model, active); } } fn close_context_menus(&mut self) -> Task { let active = self.tab_model.active(); if let Some(tab) = self.tab_model.data_mut::(active) { tab.location_context_menu_index = None; if tab.context_menu.is_some() { return self.update(Message::TabMessage( Some(active), tab::Message::ContextMenu(None, None), )); } } Task::none() } fn update_nav_model(&mut self) { let mut nav_model = segmented_button::ModelBuilder::default(); if self.config.show_recents { nav_model = nav_model.insert(|b| { b.text(fl!("recents")) .icon(icon::from_name("document-open-recent-symbolic")) .data(Location::Recents) }); } for (favorite_i, favorite) in self.config.favorites.iter().enumerate() { 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 { fl!("filesystem") }; nav_model = nav_model.insert(move |b| { b.text(name.clone()) .icon( icon::icon(if path.is_dir() { tab::folder_icon_symbolic(&path, 16) } else { icon::from_name("text-x-generic-symbolic").size(16).handle() }) .size(16), ) .data(match favorite { Favorite::Network { uri, name, path } => { Location::Network(uri.clone(), name.clone(), Some(path.to_owned())) } _ => Location::Path(path.clone()), }) .data(FavoriteIndex(favorite_i)) }); } } nav_model = nav_model.insert(|b| { b.text(fl!("trash")) .icon(icon::icon(Trash::icon_symbolic(16))) .data(Location::Trash) .divider_above() }); if !MOUNTERS.is_empty() { nav_model = nav_model.insert(|b| { b.text(fl!("networks")) .icon(icon::icon( icon::from_name("network-workgroup-symbolic") .size(16) .handle(), )) .data(Location::Network( "network:///".to_string(), fl!("networks"), None, )) .divider_above() }); } // Collect all mounter items let mut nav_items = Vec::new(); for (key, items) in &self.mounter_items { nav_items.extend(items.iter().map(|item| (*key, item))); } // Sort by name lexically nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name())); // Add items to nav model for (i, (key, item)) in nav_items.into_iter().enumerate() { nav_model = nav_model.insert(|mut b| { b = b.text(item.name()).data(MounterData(key, item.clone())); let uri = item.uri(); if let Some(path) = item.path() { if item.is_remote() { b = b.data(Location::Network(uri, item.name(), Some(path))); } else { b = b.data(Location::Path(path)); } } else if !uri.is_empty() { b = b.data(Location::Network(uri, item.name(), None)); } if let Some(icon) = item.icon(true) { b = b.icon(icon::icon(icon).size(16)); } if item.is_mounted() { b = b.closable(); } if i == 0 { b = b.divider_above(); } b }); } self.nav_model = nav_model.build(); let tab_entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data::(tab_entity) { self.activate_nav_model_location(&tab.location.clone()); } } fn update_notification(&mut self) -> Task { // Handle closing notification if there are no operations if self.pending_operations.is_empty() { #[cfg(feature = "notify")] if let Some(notification_arc) = self.notification_opt.take() { return Task::future(async move { tokio::task::spawn_blocking(move || { //TODO: this is nasty let notification_mutex = Arc::try_unwrap(notification_arc).unwrap(); let notification = notification_mutex.into_inner().unwrap(); notification.close(); }) .await .unwrap(); cosmic::action::app(Message::MaybeExit) }); } } Task::none() } fn update_title(&mut self) -> Task { let window_title = match self.tab_model.text(self.tab_model.active()) { Some(tab_title) => format!("{tab_title} — {}", fl!("cosmic-files")), None => fl!("cosmic-files"), }; if let Some(window_id) = self.core.main_window_id() { self.set_window_title(window_title, window_id) } else { Task::none() } } fn update_watcher(&mut self) -> Task { if let Some((mut watcher, old_paths)) = self.watcher_opt.take() { let new_paths: FxHashSet<_> = self .tab_model .iter() .filter_map(|entity| { let tab = self.tab_model.data::(entity)?; tab.location.path_opt().cloned() }) .collect(); // Unwatch paths no longer used for path in &old_paths { if !new_paths.contains(path) { match watcher.unwatch(path) { Ok(()) => { log::debug!("unwatching {}", path.display()); } Err(err) => { log::debug!("failed to unwatch {}: {}", path.display(), err); } } } } // Watch new paths for path in &new_paths { if !old_paths.contains(path) { match watcher.watch(path, notify::RecursiveMode::NonRecursive) { Ok(()) => { log::debug!("watching {}", path.display()); } Err(err) => { log::debug!("failed to watch {}: {}", path.display(), err); } } } } self.watcher_opt = Some((watcher, new_paths)); } //TODO: should any of this run in a command? Task::none() } fn network_drive(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, space_m, .. } = theme::spacing(); let mut table = widget::column::with_capacity(8); for (i, line) in fl!("network-drive-schemes").lines().enumerate() { let mut row = widget::row::with_capacity(2); for part in line.split(',') { row = row.push( widget::container(if i == 0 { widget::text::heading(part.to_string()) } else { widget::text::body(part.to_string()) }) .width(Length::Fill) .padding(space_xxs), ); } table = table.push(row); if i == 0 { table = table.push(widget::divider::horizontal::light()); } } widget::column::with_children([ widget::text::body(fl!("network-drive-description")).into(), table.into(), ]) .spacing(space_m) .into() } fn desktop_view_options(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_m, space_l, .. } = theme::spacing(); let config = self.config.desktop; let show_on_desktop = settings::section() .title(fl!("show-on-desktop")) .add( settings::item::builder(fl!("desktop-folder-content")).toggler( config.show_content, move |show_content| { Message::DesktopConfig(DesktopConfig { show_content, ..config }) }, ), ) .add(settings::item::builder(fl!("mounted-drives")).toggler( config.show_mounted_drives, move |show_mounted_drives| { Message::DesktopConfig(DesktopConfig { show_mounted_drives, ..config }) }, )) .add(settings::item::builder(fl!("trash-folder-icon")).toggler( config.show_trash, move |show_trash| { Message::DesktopConfig(DesktopConfig { show_trash, ..config }) }, )); let icon_size = config.icon_size; let grid_spacing = config.grid_spacing; let icon_size_and_spacing = settings::section() .title(fl!("icon-size-and-spacing")) .add( settings::item::builder(fl!("icon-size")) .description(format!("{icon_size}%")) .control( widget::slider(50..=500, icon_size.get(), move |new_value| { Message::DesktopConfig(DesktopConfig { icon_size: NonZeroU16::new(new_value).unwrap_or(icon_size), ..config }) }) .step(25u16), ), ) .add( settings::item::builder(fl!("grid-spacing")) .description(format!("{grid_spacing}%")) .control( widget::slider(50..=500, grid_spacing.get(), move |new_value| { Message::DesktopConfig(DesktopConfig { grid_spacing: NonZeroU16::new(new_value).unwrap_or(grid_spacing), ..config }) }) .step(25u16), ), ); widget::column::with_capacity(2) .padding([0, space_l, space_l, space_l]) .spacing(space_m) .push(show_on_desktop) .push(icon_size_and_spacing) .into() } fn edit_history(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_m, .. } = theme::spacing(); let mut children = Vec::new(); //TODO: get height from theme? let progress_bar_height = Length::Fixed(4.0); if !self.pending_operations.is_empty() { let mut section = widget::settings::section().title(fl!("pending")); for (id, (op, controller)) in self.pending_operations.iter().rev() { let progress = controller.progress(); section = section.add(widget::column::with_children([ widget::row::with_children([ widget::determinate_linear(progress) .width(Length::Fill) .girth(progress_bar_height) .into(), if controller.is_paused() { widget::tooltip( widget::button::icon(icon::from_name( "media-playback-start-symbolic", )) .on_press(Message::PendingPause(*id, false)) .padding(8), widget::text::body(fl!("resume")), widget::tooltip::Position::Top, ) .into() } else { widget::tooltip( widget::button::icon(icon::from_name( "media-playback-pause-symbolic", )) .on_press(Message::PendingPause(*id, true)) .padding(8), widget::text::body(fl!("pause")), widget::tooltip::Position::Top, ) .into() }, widget::tooltip( widget::button::icon(icon::from_name("window-close-symbolic")) .on_press(Message::PendingCancel(*id)) .padding(8), widget::text::body(fl!("cancel")), widget::tooltip::Position::Top, ) .into(), ]) .align_y(Alignment::Center) .into(), widget::text::body(op.pending_text(progress, controller.state())).into(), ])); } children.push(section.into()); } if !self.failed_operations.is_empty() { let mut section = widget::settings::section().title(fl!("failed")); for (op, controller, error) in self.failed_operations.values().rev() { let progress = controller.progress(); section = section.add(widget::column::with_children([ widget::text::body(op.pending_text(progress, controller.state())).into(), widget::text::body(error).into(), ])); } children.push(section.into()); } if !self.complete_operations.is_empty() { let mut section = widget::settings::section().title(fl!("complete")); for op in self.complete_operations.values().rev() { section = section.add(widget::text::body(op.completed_text())); } children.push(section.into()); } if children.is_empty() { children.push(widget::text::body(fl!("no-history")).into()); } widget::column::with_children(children) .spacing(space_m) .into() } fn preview<'a>( &'a self, entity_opt: &Option, kind: &'a PreviewKind, context_drawer: bool, ) -> Element<'a, tab::Message> { let cosmic_theme::Spacing { space_l, .. } = theme::spacing(); let mut children = Vec::with_capacity(1); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let military_time = self.config.tab.military_time; match kind { PreviewKind::Custom(PreviewItem(item)) => { children.push(item.preview_view(Some(&self.mime_app_cache), military_time)); } PreviewKind::Location(location) => { if let Some(tab) = self.tab_model.data::(entity) && let Some(items) = tab.items_opt() { for item in items { if item.location_opt.as_ref() == Some(location) { children .push(item.preview_view(Some(&self.mime_app_cache), military_time)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; } } } } PreviewKind::Selected => { if let Some(tab) = self.tab_model.data::(entity) && let Some(items) = tab.items_opt() { let preview_opt = { let mut selected = items.iter().filter(|item| item.selected); match (selected.next(), selected.next()) { // At least two selected items (Some(_), Some(_)) => { Some(tab.multi_preview_view(Some(&self.mime_app_cache))) } // Exactly one selected item (Some(item), None) => { Some(item.preview_view(Some(&self.mime_app_cache), military_time)) } // No selected items _ => None, } }; if let Some(preview) = preview_opt { children.push(preview); } if children.is_empty() && let Some(item) = &tab.parent_item_opt { children.push(item.preview_view(Some(&self.mime_app_cache), military_time)); } } } } widget::column::with_children(children) .padding(if context_drawer { [0, 0, 0, 0] } else { [0, space_l, space_l, space_l] }) .into() } fn settings(&self) -> Element<'_, Message> { let tab_config = self.config.tab; // TODO: Should dialog be updated here too? settings::view_column(vec![ settings::section() .title(fl!("appearance")) .add({ let app_theme_selected = match self.config.app_theme { AppTheme::Dark => 1, AppTheme::Light => 2, AppTheme::System => 0, }; settings::item::builder(fl!("theme")).control(widget::dropdown( &self.app_themes, Some(app_theme_selected), move |index| { Message::AppTheme(match index { 1 => AppTheme::Dark, 2 => AppTheme::Light, _ => AppTheme::System, }) }, )) }) .into(), settings::section() .title(fl!("type-to-search")) .add( settings::item::builder(fl!("type-to-search-recursive")).radio( TypeToSearch::Recursive, Some(self.config.type_to_search), Message::SetTypeToSearch, ), ) .add( settings::item::builder(fl!("type-to-search-enter-path")).radio( TypeToSearch::EnterPath, Some(self.config.type_to_search), Message::SetTypeToSearch, ), ) .add(settings::item::builder(fl!("type-to-search-select")).radio( TypeToSearch::SelectByPrefix, Some(self.config.type_to_search), Message::SetTypeToSearch, )) .into(), settings::section() .title(fl!("other")) .add({ settings::item::builder(fl!("single-click")).toggler( tab_config.single_click, move |single_click| { Message::TabConfig(TabConfig { single_click, ..tab_config }) }, ) }) .add({ settings::item::builder(fl!("show-recents")) .toggler(self.config.show_recents, Message::SetShowRecents) }) .into(), ]) .into() } fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> { let mut results = Vec::new(); let mut dedupe = FxHashSet::default(); // start with exact matches results.extend( self.mime_app_cache .get(mime_type) .iter() .filter(|&mime_app| dedupe.insert(&mime_app.id)) .map(|mime_app| (mime_app, MimeAppMatch::Exact)), ); // grab matches based off of subclass / parent mime type if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) { for parent_type in parent_types { results.extend( self.mime_app_cache .get(&parent_type) .iter() .filter(|&mime_app| dedupe.insert(&mime_app.id)) .map(|mime_app| (mime_app, MimeAppMatch::Related)), ); } } // Add other apps results.extend( self.mime_app_cache .apps() .iter() .filter(|&mime_app| dedupe.insert(&mime_app.id)) .map(|mime_app| (mime_app, MimeAppMatch::Other)), ); results } // Update favorites based on renaming or moving dirs. fn update_favorites(&mut self, path_changes: &[(impl AsRef, impl AsRef)]) -> bool { let mut favorites_changed = false; let favorites = self .config .favorites .iter() .map(|favorite| { if let Favorite::Path(path) = favorite { for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { if path.starts_with(from) && let Ok(relative) = path.strip_prefix(from) { favorites_changed = true; return Favorite::from_path(to.join(relative)); } } } favorite.clone() }) .collect(); if favorites_changed { if let Some(config_handler) = &self.config_handler { match self.config.set_favorites(config_handler, favorites) { Ok(updated) => { if updated { return true; } } Err(err) => { log::warn!("failed to update favorites after moving directories: {err:?}",); } } } else { self.config.favorites = favorites; log::warn!( "failed to update favorites after moving directories: no config handler", ); } } false } } /// Implement [`Application`] to integrate with COSMIC. impl Application for App { /// Default async executor to use with the app. type Executor = executor::Default; /// Argument received type Flags = Flags; /// Message type specific to our [`App`]. type Message = Message; /// The unique application ID to supply to the window manager. const APP_ID: &'static str = "com.system76.CosmicFiles"; fn core(&self) -> &Core { &self.core } fn core_mut(&mut self) -> &mut Core { &mut self.core } /// Creates the application, and optionally emits command on initialize. fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { core.window.context_is_overlay = false; match flags.mode { Mode::App => { core.window.show_context = flags.config.show_details; } Mode::Desktop => { core.window.content_container = false; core.window.show_window_menu = false; core.window.show_headerbar = false; core.window.sharp_corners = false; core.window.show_maximize = false; core.window.show_minimize = false; core.window.use_template = true; } } let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")]; let key_binds = key_binds(&match flags.mode { Mode::App => tab::Mode::App, Mode::Desktop => tab::Mode::Desktop, }); // Create a dedicated thread for the compio runtime to handle operations on. // Supports io_uring on Linux, IOPC on Windows, and polling everywhere else. let (compio_tx, mut compio_rx) = mpsc::channel(1); let tokio_handle = tokio::runtime::Handle::current(); std::thread::spawn(move || { let _tokio = tokio_handle.enter(); compio::runtime::RuntimeBuilder::new() .build() .unwrap() .block_on(async move { while let Some(task) = compio_rx.recv().await { compio::runtime::spawn(task).detach(); } }); }); let about = About::default() .name(fl!("cosmic-files")) .icon(icon::from_name(Self::APP_ID)) .version(env!("CARGO_PKG_VERSION")) .author("System76") .comments(fl!("comment")) .license("GPL-3.0-only") .license_url("https://spdx.org/licenses/GPL-3.0-only") .developers([("Jeremy Soller", "jeremy@system76.com")]) .links([ (fl!("repository"), "https://github.com/pop-os/cosmic-files"), ( fl!("support"), "https://github.com/pop-os/cosmic-files/issues", ), ]); let mut app = Self { core, about, nav_bar_context_id: segmented_button::Entity::null(), nav_model: segmented_button::ModelBuilder::default().build(), tab_model: segmented_button::ModelBuilder::default().build(), config_handler: flags.config_handler, state_handler: flags.state_handler, config: flags.config, state: flags.state, mode: flags.mode, app_themes, compio_tx, context_page: ContextPage::Preview(None, PreviewKind::Selected), dialog_pages: DialogPages::new(), dialog_text_input: widget::Id::new("Dialog Text Input"), key_binds, margin: FxHashMap::default(), mime_app_cache: MimeAppCache::new(), modifiers: Modifiers::empty(), mounter_items: FxHashMap::default(), must_save_sort_names: false, network_drive_connecting: None, network_drive_input: String::new(), #[cfg(feature = "notify")] notification_opt: None, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] overlap: FxHashMap::default(), pending_operation_id: 0, pending_operations: BTreeMap::new(), progress_operations: BTreeSet::new(), complete_operations: BTreeMap::new(), failed_operations: BTreeMap::new(), scrollable_id: widget::Id::new("File Scrollable"), search_id: widget::Id::new("File Search"), size: None, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] surface_ids: FxHashMap::default(), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] surface_names: FxHashMap::default(), toasts: widget::toaster::Toasts::new(Message::CloseToast), watcher_opt: None, windows: FxHashMap::default(), nav_dnd_hover: None, tab_dnd_hover: None, type_select_prefix: String::new(), type_select_last_key: None, nav_drag_id: DragId::new(), tab_drag_id: DragId::new(), auto_scroll_speed: None, file_dialog_opt: None, clipboard_cache: ClipboardCache::Empty, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] layer_sizes: FxHashMap::default(), }; let mut commands = vec![app.update_config(), app.update(Message::CheckClipboard)]; for location in flags.locations { if let Some(path) = location.path_opt() && path.is_file() && let Some(parent) = path.parent() { commands.push(app.open_tab( Location::Path(parent.to_path_buf()), true, Some(vec![path.clone()]), )); continue; } commands.push(app.open_tab(location, true, None)); } for location in flags.uris { if let Some(e) = app.nav_model.iter().find(|e| { app.nav_model.data::(*e).is_some_and( |l| matches!(l, Location::Network(uri, ..) if *uri == *location.as_str()), ) }) { commands.push(cosmic::task::message(cosmic::Action::App( Message::NetworkDriveOpenEntityAfterMount { entity: e }, ))); } } if app.tab_model.entity_at(0).is_none() { if let Ok(current_dir) = env::current_dir() { commands.push(app.open_tab(Location::Path(current_dir), true, None)); } else { commands.push(app.open_tab(Location::Path(home_dir()), true, None)); } } (app, Task::batch(commands)) } fn nav_bar(&self) -> Option>> { if !self.core.nav_bar_active() { return None; } let nav_model = self.nav_model()?; let mut nav = cosmic::widget::nav_bar(nav_model, |entity| { cosmic::Action::Cosmic(cosmic::app::Action::NavBar(entity)) }) .drag_id(self.nav_drag_id) .on_dnd_enter(|entity, _| cosmic::Action::App(Message::DndEnterNav(entity))) .on_dnd_leave(|_| cosmic::Action::App(Message::DndExitNav)) .on_dnd_drop(|entity, data, action| { cosmic::Action::App(Message::DndDropNav(entity, data, action)) }) .on_context(|entity| cosmic::Action::App(Message::NavBarContext(entity))) .on_close(|entity| cosmic::Action::App(Message::NavBarClose(entity))) .on_middle_press(|entity| { cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity))) }) .context_menu(self.nav_context_menu(self.nav_bar_context_id)) .close_icon(icon::from_name("media-eject-symbolic").size(16).icon()) .into_container(); if !self.core.is_condensed() { nav = nav.max_width(280); } Some(Element::from( nav.width(Length::Shrink).height(Length::Fill), )) } fn nav_context_menu( &self, entity: widget::nav_bar::Id, ) -> Option>>> { let favorite_index_opt = self.nav_model.data::(entity); let location_opt = self.nav_model.data::(entity); let mut items = Vec::with_capacity(7); if location_opt .and_then(Location::path_opt) .is_some_and(|x| x.is_file()) { items.push(cosmic::widget::menu::Item::Button( fl!("open"), None, NavMenuAction::Open(entity), )); items.push(cosmic::widget::menu::Item::Button( fl!("menu-open-with"), None, NavMenuAction::OpenWith(entity), )); } else { items.push(cosmic::widget::menu::Item::Button( fl!("open-in-new-tab"), None, NavMenuAction::OpenInNewTab(entity), )); items.push(cosmic::widget::menu::Item::Button( fl!("open-in-new-window"), None, NavMenuAction::OpenInNewWindow(entity), )); } if let Some(path) = location_opt.and_then(Location::path_opt) { let selected_dir = usize::from(path.is_dir()); let action_items: Vec<_> = self .config .context_actions .iter() .enumerate() .filter(|(_, action)| action.matches_selection(1, selected_dir)) .map(|(i, action)| { cosmic::widget::menu::Item::Button( action.name.clone(), None, NavMenuAction::RunContextAction(entity, i), ) }) .collect(); if !action_items.is_empty() { items.push(cosmic::widget::menu::Item::Divider); items.extend(action_items); } } items.push(cosmic::widget::menu::Item::Divider); if matches!(location_opt, Some(Location::Path(..))) { items.push(cosmic::widget::menu::Item::Button( fl!("show-details"), None, NavMenuAction::Preview(entity), )); } items.push(cosmic::widget::menu::Item::Divider); if favorite_index_opt.is_some() { items.push(cosmic::widget::menu::Item::Button( fl!("remove-from-sidebar"), None, NavMenuAction::RemoveFromSidebar(entity), )); } if matches!(location_opt, Some(Location::Recents)) && tab::has_recents() { items.push(cosmic::widget::menu::Item::Button( fl!("clear-recents-history"), None, NavMenuAction::ClearRecents, )); } if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() { items.push(cosmic::widget::menu::Item::Button( fl!("empty-trash"), None, NavMenuAction::EmptyTrash, )); } Some(cosmic::widget::menu::items(&HashMap::new(), items)) } fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> { match self.mode { Mode::App => Some(&self.nav_model), Mode::Desktop => None, } } fn on_nav_select(&mut self, entity: Entity) -> Task { self.nav_model.activate(entity); if let Some(location) = self.nav_model.data::(entity) { let should_open = match location { #[cfg(feature = "gvfs")] Location::Network(uri, name, Some(path)) if !path.try_exists().unwrap_or_default() => { let mut found = false; if let Some(key) = self .mounter_items .iter() .find_map(|(k, items)| { items.iter().find_map(|item| { found |= item.path().is_some_and(|p| path.starts_with(&p)) || item.name() == *name || item.uri() == *uri; (!item.is_mounted() && found).then_some(*k) }) }) .or(if found { None } else { // TODO do we need to choose the correct mounter? self.mounter_items.keys().copied().next() }) && let Some(mounter) = MOUNTERS.get(&key) { return mounter.network_drive(uri.clone()).map(move |()| { cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount { entity, }) }); } log::warn!( "failed to open favorite, path does not exist: {}", path.display() ); return self.push_dialog( DialogPage::FavoritePathError { path: path.clone(), entity, }, Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), ); } Location::Path(path) | Location::Network(_, _, Some(path)) => { match path.try_exists() { Ok(true) => true, Ok(false) => { log::warn!( "failed to open favorite, path does not exist: {}", path.display() ); return self.push_dialog( DialogPage::FavoritePathError { path: path.clone(), entity, }, Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), ); } Err(err) => { log::warn!( "failed to open favorite for path: {}, {}", path.display(), err ); return self.push_dialog( DialogPage::FavoritePathError { path: path.clone(), entity, }, Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), ); } } } _ => true, }; if should_open { let message = Message::TabMessage(None, tab::Message::Location(location.clone())); return self.update(message); } } if let Some(data) = self.nav_model.data::(entity) && let Some(mounter) = MOUNTERS.get(&data.0) { return mounter .mount(data.1.clone()) .map(|()| cosmic::action::none()); } Task::none() } fn on_app_exit(&mut self) -> Option { Some(Message::WindowClose) } fn on_close_requested(&self, id: window::Id) -> Option { Some(Message::WindowCloseRequested(id)) } fn on_context_drawer(&mut self) -> Task { if let ContextPage::Preview(..) = self.context_page { // Persist state of preview page if self.core.window.show_context != self.config.show_details { return self.update(Message::Preview(None)); } } Task::none() } fn on_escape(&mut self) -> Task { let entity = self.tab_model.active(); // Close dialog if open if let Some((_page, task)) = self.dialog_pages.pop_front() { return task; } // Close gallery mode if open if let Some(tab) = self.tab_model.data_mut::(entity) && tab.gallery { tab.gallery = false; return Task::none(); } // Close menus and context panes in order per message // Why: It'd be weird to close everything all at once // Usually, the Escape key (for example) closes menus and panes one by one instead // of closing everything on one press if self.core.window.show_context { self.set_show_context(false); return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false))); } if let Some(tab) = self.tab_model.data_mut::(entity) { if tab.location_context_menu_index.is_some() { tab.location_context_menu_index = None; return Task::none(); } if tab.context_menu.is_some() { return self.update(Message::TabMessage( Some(entity), tab::Message::ContextMenu(None, None), )); } if tab.edit_location.is_some() { tab.edit_location = None; return Task::none(); } let had_focused_button = tab.select_focus_id().is_some(); if tab.select_none() { if had_focused_button { // Unfocus if there was a focused button return widget::button::focus(widget::Id::unique()); } return Task::none(); } } if self.search_get().is_some() { // Close search if open return self.search_set_active(None); } Task::none() } /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { // Helper for updating config values efficiently macro_rules! config_set { ($name: ident, $value: expr) => { match &self.config_handler { Some(config_handler) => { match paste::paste! { self.config.[](config_handler, $value) } { Ok(_) => {} Err(err) => { log::warn!( "failed to save config {:?}: {}", stringify!($name), err ); } } } None => { self.config.$name = $value; log::warn!( "failed to save config {:?}: no config handler", stringify!($name) ); } } }; } match message { Message::AddToSidebar(entity_opt) => { let mut favorites = self.config.favorites.clone(); // check if the selected entity is in the current tab // else just use the selected entity and check its location let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); for path in self.selected_paths(entity_opt) { let is_network = self.tab_model.data::(entity).and_then(|tab| { let in_current_tab = tab .location .path_opt() .zip(path.parent()) .is_some_and(|(t_path, parent)| parent == t_path); let tab = if in_current_tab { self.tab_model .data::(self.tab_model.active()) .unwrap_or(tab) } else { tab }; let name = Location::Path(path.clone()).title(); if let Location::Network(uri, _, _) = tab .items_opt .as_ref() .and_then(|items| items.iter().find(|&i| i.path_opt() == Some(&path))) .unwrap() .location_opt .as_ref() .unwrap() { Some((uri.clone(), name, path.clone())) } else { None } }); let name = Location::Path(path.clone()).title(); let favorite = if let Some((uri, _, _)) = is_network.clone() { Favorite::Network { uri, name, path } } else { Favorite::from_path(path) }; if !favorites.contains(&favorite) { favorites.push(favorite); } } config_set!(favorites, favorites); return self.update_config(); } Message::AppTheme(app_theme) => { config_set!(app_theme, app_theme); return self.update_config(); } Message::Compress(entity_opt) => { let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if let Some(current_path) = paths.first() && let Some(destination) = current_path.parent().zip(current_path.file_stem()) { let to = destination.0.to_path_buf(); let name = destination.1.to_str().unwrap_or_default().to_string(); let archive_type = ArchiveType::default(); return self.push_dialog( DialogPage::Compress { paths, to, name, archive_type, password: None, }, Some(self.dialog_text_input.clone()), ); } } Message::Config(config) => { if config != self.config { log::info!("update config"); // Show details is preserved for existing instances let show_details = self.config.show_details; self.config = config; self.config.show_details = show_details; return self.update_config(); } } Message::Copy(entity_opt) => { if let Some(entity) = entity_opt && let Some(tab) = self.tab_model.data_mut::(entity) { tab.refresh_cut(&[]); } let paths = self.selected_paths(entity_opt); self.clipboard_cache = ClipboardCache::Files(ClipboardPaste { paths: paths.map(|p| p.to_path_buf()).collect(), kind: ClipboardKind::Copy, }); let contents = ClipboardCopy::new(ClipboardKind::Copy, self.selected_paths(entity_opt)); return clipboard::write_data(contents); } Message::CopyPath(entity_opt) => { let paths = self.selected_paths(entity_opt); let path_strings: Vec = paths.into_iter().map(|p| p.display().to_string()).collect(); let text = path_strings.join("\n"); return clipboard::write(text); } Message::CopyTo(entity_opt) => { let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return self.copy_to(&selected_paths); } Message::CopyToResult(result) => { match result { DialogResult::Cancel => {} DialogResult::Open(selected_paths) => { let mut file_paths = None; if let Some(file_dialog) = &self.file_dialog_opt && let Some(window) = self.windows.remove(&file_dialog.window_id()) && let WindowKind::FileDialog(paths) = window.kind { file_paths = paths; } if let Some(file_paths) = file_paths && !selected_paths.is_empty() { self.file_dialog_opt = None; return self.operation(Operation::Copy { paths: file_paths.to_vec(), to: selected_paths[0].clone(), }); } } } self.file_dialog_opt = None; } Message::Cut(entity_opt) => { self.set_cut(entity_opt); let paths = self.selected_paths(entity_opt); self.clipboard_cache = ClipboardCache::Files(ClipboardPaste { paths: paths.map(|p| p.to_path_buf()).collect(), kind: ClipboardKind::Cut { is_dnd: false }, }); let contents = ClipboardCopy::new( ClipboardKind::Cut { is_dnd: false }, self.selected_paths(entity_opt), ); return clipboard::write_data(contents); } Message::CloseToast(id) => { self.toasts.remove(id); } Message::CosmicSettings(arg) => { //TODO: use special settings URL scheme instead? let mut command = process::Command::new("cosmic-settings"); command.arg(arg); match spawn_detached(&mut command) { Ok(()) => {} Err(err) => { log::warn!("failed to run cosmic-settings {arg}: {err}"); } } } Message::Delete(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data::(entity) { if tab.location.is_trash() { if let Some(items) = tab.items_opt() { let mut trash_items = Vec::new(); for item in items { if item.selected { if let ItemMetadata::Trash { entry, .. } = &item.metadata { trash_items.push(entry.clone()); } else { //TODO: error on trying to permanently delete non-trash file? } } } if !trash_items.is_empty() { return self.update(Message::DialogPush( DialogPage::DeleteTrash { items: trash_items }, Some(DELETE_TRASH_BUTTON_ID.clone()), )); } } } else { let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if !paths.is_empty() { return self.delete(paths); } } } } Message::DesktopConfig(config) => { if config != self.config.desktop { config_set!(desktop, config); return self.update_desktop(); } } Message::DesktopViewOptions => { let mut settings = window::Settings { decorations: true, min_size: Some(Size::new(360.0, 180.0)), resizable: true, size: Size::new(480.0, 444.0), transparent: true, ..Default::default() }; #[cfg(target_os = "linux")] { // Use the dialog ID to make it float settings.platform_specific.application_id = "com.system76.CosmicFilesDialog".to_string(); } let (id, command) = window::open(settings); self.windows .insert(id, Window::new(WindowKind::DesktopViewOptions)); return command.map(|_id| cosmic::action::none()); } Message::DesktopDialogs(show) => { if matches!(self.mode, Mode::Desktop) { if show { //TODO: would it be better to make this a layer surface? let mut settings = window::Settings { decorations: false, level: window::Level::AlwaysOnTop, max_size: Some(Size::new(1280.0, 640.0)), min_size: Some(Size::new(320.0, 180.0)), position: window::Position::Centered, resizable: false, size: Size::new(640.0, 320.0), transparent: true, ..Default::default() }; #[cfg(target_os = "linux")] { // Use the dialog ID to make it float settings.platform_specific.application_id = "com.system76.CosmicFilesDialog".to_string(); } let (id, command) = window::open(settings); self.windows .insert(id, Window::new(WindowKind::Dialogs(widget::Id::unique()))); return command.map(|_id| cosmic::Action::None); } let tasks = self .windows .iter() .filter(|(_, window)| matches!(window.kind, WindowKind::Dialogs(_))) .map(|(id, _)| window::close(*id)); return Task::batch(tasks); } } Message::DialogCancel => { if let Some((_page, task)) = self.dialog_pages.pop_front() { return task; } } Message::DialogComplete => { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() { let mut tasks = vec![task]; match dialog_page { DialogPage::Compress { paths, to, name, archive_type, password, } => { let extension = archive_type.extension(); let name = format!("{name}{extension}"); let to = to.join(name); tasks.push(self.operation(Operation::Compress { paths: paths.into_vec(), to, archive_type, password, })); } DialogPage::EmptyTrash => { tasks.push(self.operation(Operation::EmptyTrash)); } DialogPage::FailedOperation(id) => { log::warn!("TODO: retry operation {id}"); } DialogPage::FailedOperations(_ids) => { log::warn!("TODO: retry operations"); } DialogPage::ExtractPassword { id, password } => { let (operation, _, _err) = self.failed_operations.get(&id).unwrap(); let new_op = match &operation { Operation::Extract { to, paths, .. } => Operation::Extract { to: to.clone(), paths: paths.clone(), password: Some(password), }, _ => unreachable!(), }; tasks.push(self.operation(new_op)); } DialogPage::MountError { mounter_key, item, error: _, } => { if let Some(mounter) = MOUNTERS.get(&mounter_key) { tasks.push(mounter.mount(item).map(|()| cosmic::action::none())); } } DialogPage::NetworkAuth { mounter_key: _, uri: _, auth, auth_tx, } => { tasks.push(Task::future(async move { auth_tx.send(auth).await.unwrap(); cosmic::action::none() })); } DialogPage::NetworkError { mounter_key: _, uri, error: _, } => { //TODO: re-use mounter_key? tasks.push(self.update(Message::NetworkDriveInput(uri))); tasks.push(self.update(Message::NetworkDriveSubmit)); } DialogPage::NewItem { parent, name, dir } => { let path = parent.join(name); tasks.push(self.operation(if dir { Operation::NewFolder { path } } else { Operation::NewFile { path } })); } DialogPage::RunContextAction { action, paths } => { context_action::run(&self.config.context_actions, action, &paths); } DialogPage::OpenWith { path, mime, selected, .. } => { let available_apps = self.get_apps_for_mime(&mime); if let Some((app, _)) = available_apps.get(selected) { if let Some(mut command) = app.command(&[&path]).and_then(|v| v.into_iter().next()) { match spawn_detached(&mut command) { Ok(()) => { if self.config.show_recents { let _ = recently_used_xbel::update_recently_used( &path, Self::APP_ID.to_string(), "cosmic-files".to_string(), None, ); } } Err(err) => { log::warn!( "failed to open {} with {:?}: {}", path.display(), app.id, err ); } } } else { log::warn!( "failed to open {} with {:?}: failed to get command", path.display(), app.id ); } } } DialogPage::PermanentlyDelete { paths } => { tasks.push(self.operation(Operation::PermanentlyDelete { paths })); } DialogPage::DeleteTrash { items } => { tasks.push(self.operation(Operation::DeleteTrash { items })); } DialogPage::RenameItem { from, parent, name, .. } => { let to = parent.join(name); tasks.push(self.operation(Operation::Rename { from, to })); } DialogPage::Replace { .. } => { log::warn!("replace dialog should be completed with replace result"); } DialogPage::SetExecutableAndLaunch { path } => { tasks.push(self.operation(Operation::SetExecutableAndLaunch { path })); } DialogPage::FavoritePathError { entity, .. } => { if let Some(FavoriteIndex(favorite_i)) = self.nav_model.data::(entity) { let mut favorites = self.config.favorites.clone(); favorites.remove(*favorite_i); config_set!(favorites, favorites); tasks.push(self.update_config()); } } } return Task::batch(tasks); } } Message::DialogPush(dialog_page, focused_id) => { return self.push_dialog(dialog_page, focused_id); } Message::DialogUpdate(dialog_page) => { self.dialog_pages.update_front(dialog_page); } Message::DialogUpdateComplete(dialog_page) => { return Task::batch([ self.update(Message::DialogUpdate(dialog_page)), self.update(Message::DialogComplete), ]); } Message::ExtractHere(entity_opt) => { let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if let Some(destination) = paths .first() .and_then(|first| first.parent()) .map(Path::to_path_buf) { return self.operation(Operation::Extract { paths, to: destination, password: None, }); } } Message::ExtractTo(entity_opt) => { let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return self.extract_to(&selected_paths); } Message::ExtractToResult(result) => { match result { DialogResult::Cancel => {} DialogResult::Open(selected_paths) => { let mut archive_paths = None; if let Some(file_dialog) = &self.file_dialog_opt && let Some(window) = self.windows.remove(&file_dialog.window_id()) && let WindowKind::FileDialog(paths) = window.kind { archive_paths = paths; } if let Some(archive_paths) = archive_paths && !selected_paths.is_empty() { self.file_dialog_opt = None; return self.operation(Operation::Extract { paths: archive_paths, to: selected_paths[0].clone(), password: None, }); } } } self.file_dialog_opt = None; } Message::FileDialogMessage(dialog_message) => { if let Some(dialog) = &mut self.file_dialog_opt { return dialog.update(dialog_message); } } Message::Key(window_id, modifiers, key, text) => { #[cfg(all(feature = "wayland", feature = "desktop-applet"))] let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id); #[cfg(not(all(feature = "wayland", feature = "desktop-applet")))] let in_surface_ids = false; if self.core.main_window_id() == Some(window_id) || in_surface_ids { let entity = self.tab_model.active(); for (key_bind, action) in &self.key_binds { if key_bind.matches(modifiers, &key) { return self.update(action.message(Some(entity))); } } // Uncaptured keys with only shift modifiers go to the search or location box if matches!(self.mode, Mode::App) && !modifiers.logo() && !modifiers.control() && !modifiers.alt() && matches!(key, Key::Character(_)) && let Some(text) = text { match self.config.type_to_search { TypeToSearch::Recursive => { let mut term = self.search_get().unwrap_or_default().to_string(); term.push_str(&text); return self.search_set_active(Some(term)); } TypeToSearch::EnterPath => { if let Some(tab) = self.tab_model.data_mut::(entity) { let location = tab .edit_location .as_ref() .map_or_else(|| &tab.location, |x| &x.location); // Try to add text to end of location if let Location::Network(uri, ..) = location { let mut uri_string = uri.clone(); uri_string.push_str(&text); tab.edit_location = Some(location.with_uri(uri_string).into()); } else if let Some(path) = location.path_opt() { let mut path_string = path.to_string_lossy().into_owned(); path_string.push_str(&text); tab.edit_location = Some(location.with_path(path_string.into()).into()); } } } TypeToSearch::SelectByPrefix => { // Reset buffer if timeout elapsed if let Some(last_key) = self.type_select_last_key && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { self.type_select_prefix.clear(); } // Accumulate character and select self.type_select_prefix.push_str(&text.to_lowercase()); self.type_select_last_key = Some(Instant::now()); if let Some(tab) = self.tab_model.data_mut::(entity) { tab.select_by_prefix(&self.type_select_prefix); if let Some(offset) = tab.select_focus_scroll() { return scrollable::scroll_to( tab.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ); } } } } } } } Message::MaybeExit => { if self.core.main_window_id().is_none() && self.pending_operations.is_empty() { // Exit if window is closed and there are no pending operations process::exit(0); } } Message::LaunchUrl(url) => match open::that_detached(&url) { Ok(()) => {} Err(err) => { log::warn!("failed to open {url:?}: {err}"); } }, Message::ModifiersChanged(window_id, modifiers) => { #[cfg(all(feature = "wayland", feature = "desktop-applet"))] let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id); #[cfg(not(all(feature = "wayland", feature = "desktop-applet")))] let in_surface_ids = false; if self.core.main_window_id() == Some(window_id) || in_surface_ids { self.modifiers = modifiers; } if let Some(window) = self.windows.get_mut(&window_id) { window.modifiers = modifiers; } } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders let mut unmounted = Vec::new(); if let Some(old_items) = self.mounter_items.get(&mounter_key) { for old_item in old_items { if let Some(old_path) = old_item.path() && old_item.is_mounted() { let mut still_mounted = false; for item in &mounter_items { if let Some(path) = item.path() && path == old_path && item.is_mounted() { still_mounted = true; break; } } if !still_mounted { unmounted.push(old_path); } } } } // Go back to home in any tabs that were unmounted let mut commands = Vec::new(); { let home_location = Location::Path(home_dir()); let entities: Box<[_]> = self.tab_model.iter().collect(); for entity in entities { let title_opt = self.tab_model.data_mut::(entity).and_then(|tab| { unmounted .iter() .any(|unmounted| { tab.location .path_opt() .is_some_and(|location| location.starts_with(unmounted)) }) .then(|| { tab.change_location(&home_location, None); tab.title() }) }); if let Some(title) = title_opt { self.tab_model.text_set(entity, title); commands.push(self.update_tab(entity, home_location.clone(), None)); } } if !commands.is_empty() { commands.push(self.update_title()); commands.push(self.update_watcher()); } } // Insert new items self.mounter_items.insert(mounter_key, mounter_items); // Update nav bar //TODO: this could change favorites IDs while they are in use self.update_nav_model(); // Update desktop tabs commands.push(self.update_desktop()); return Task::batch(commands); } Message::MountResult(mounter_key, item, res) => match res { Ok(true) => { log::info!("connected to {item:?}"); // Automatically navigate to the mounted location if let Some(path) = item.path() { let location = if item.is_remote() { Location::Network(item.uri(), item.name(), Some(path)) } else { Location::Path(path) }; let message = Message::TabMessage(None, tab::Message::Location(location)); return self.update(message); } } Ok(false) => { log::info!("cancelled connection to {item:?}"); } Err(error) => { log::warn!("failed to connect to {item:?}: {error}"); return self.push_dialog( DialogPage::MountError { mounter_key, item, error, }, Some(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()), ); } }, Message::Mouse(window_id, _button) => { // Close context menu when clicking outside. if self.core.main_window_id() == Some(window_id) { return self.close_context_menus(); } } Message::MoveTo(entity_opt) => { let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return self.move_to(&selected_paths); } Message::MoveToResult(result) => { match result { DialogResult::Cancel => {} DialogResult::Open(selected_paths) => { let mut file_paths = None; if let Some(file_dialog) = &self.file_dialog_opt && let Some(window) = self.windows.remove(&file_dialog.window_id()) && let WindowKind::FileDialog(paths) = window.kind { file_paths = paths; } if let Some(file_paths) = file_paths && !selected_paths.is_empty() { self.file_dialog_opt = None; return self.operation(Operation::Move { paths: file_paths.to_vec(), to: selected_paths[0].clone(), cross_device_copy: false, }); } } } self.file_dialog_opt = None; } Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => { return self.push_dialog( DialogPage::NetworkAuth { mounter_key, uri, auth, auth_tx, }, Some(self.dialog_text_input.clone()), ); } Message::NetworkDriveInput(input) => { self.network_drive_input = input; } Message::NetworkDriveSubmit => { //TODO: know which mounter to use for network drives if let Some((mounter_key, mounter)) = MOUNTERS.iter().next() { self.network_drive_connecting = Some((*mounter_key, self.network_drive_input.clone())); return mounter .network_drive(self.network_drive_input.clone()) .map(|()| cosmic::action::none()); } log::warn!( "no mounter found for connecting to {:?}", self.network_drive_input ); } Message::NetworkResult(mounter_key, uri, res) => { if self .network_drive_connecting .as_ref() .is_some_and(|(m, u)| *m == mounter_key && *u == uri) { self.network_drive_connecting = None; } match res { Ok(true) => { log::info!("connected to {uri:?}"); if matches!(self.context_page, ContextPage::NetworkDrive) { self.set_show_context(false); } } Ok(false) => { log::info!("cancelled connection to {uri:?}"); } Err(error) => { log::warn!("failed to connect to {uri:?}: {error}"); return self.dialog_pages.push_back(DialogPage::NetworkError { mounter_key, uri, error, }); } } } Message::NewItem(entity_opt, dir) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(path) = tab.location.path_opt() { return Task::batch([ self.dialog_pages.push_back(DialogPage::NewItem { parent: path.clone(), name: String::new(), dir, }), widget::text_input::focus(self.dialog_text_input.clone()), ]); } } #[cfg(feature = "notify")] Message::Notification(notification) => { self.notification_opt = Some(notification); } Message::NotifyEvents(events) => { log::debug!("{events:?}"); let mut needs_reload = Vec::new(); let entities: Box<[_]> = self.tab_model.iter().collect(); for entity in entities { if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(path) = tab.location.path_opt() { let mut contains_change = false; for event in &events { for event_path in &event.paths { if event_path.starts_with(path) { if let notify::EventKind::Modify( notify::event::ModifyKind::Metadata(_) | notify::event::ModifyKind::Data(_), ) = event.kind { // If metadata or data changed, find the matching item and reload it //TODO: this could be further optimized by looking at what exactly changed if let Some(items) = &mut tab.items_opt { for item in items.iter_mut() { if item.path_opt() == Some(event_path) { //TODO: reload more, like mime types? match fs::metadata(event_path) { Ok(new_metadata) => { if let ItemMetadata::Path { metadata, .. } = &mut item.metadata { *metadata = new_metadata; } } Err(err) => { log::warn!( "failed to reload metadata for {}: {}", path.display(), err ); } } //TODO item.thumbnail_opt = } } } } else { // Any other events reload the whole tab contains_change = true; break; } } } } if contains_change { needs_reload.push((entity, tab.location.clone())); } } } let commands = needs_reload .into_iter() .map(|(entity, location)| self.update_tab(entity, location, None)); return Task::batch(commands); } Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take() { Some(watcher) => { self.watcher_opt = Some((watcher, FxHashSet::default())); return self.update_watcher(); } None => { log::warn!("message did not contain notify watcher"); } }, Message::OpenTerminal(entity_opt) => { if let Some(terminal) = self.mime_app_cache.terminal() { let mut paths = Box::from([]); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(path) = tab.location.path_opt() { if let Some(items) = tab.items_opt() { paths = items .iter() .filter_map( |item| { if item.selected { item.path_opt() } else { None } }, ) .collect(); } if paths.is_empty() { paths = Box::from([path]); } } for path in paths { if let Some(mut command) = terminal .command::<&str>(&[]) .and_then(|v| v.into_iter().next()) { command.current_dir(path); if let Err(err) = spawn_detached(&mut command) { log::warn!( "failed to open {} with terminal {:?}: {}", path.display(), terminal.id, err ); } } else { log::warn!("failed to get command for {:?}", terminal.id); } } } } Message::OpenInNewTab(entity_opt) => { let selected_paths: Box<[_]> = self .selected_paths(entity_opt) .filter(|p| p.is_dir()) .collect(); return Task::batch( selected_paths .into_iter() .map(|path| self.open_tab(Location::Path(path), false, None)), ); } Message::OpenInNewWindow(entity_opt) => match env::current_exe() { Ok(exe) => self .selected_paths(entity_opt) .filter(|p| p.is_dir()) .for_each(|path| match process::Command::new(&exe).arg(path).spawn() { Ok(_child) => {} Err(err) => { log::error!("failed to execute {}: {}", exe.display(), err); } }), Err(err) => { log::error!("failed to get current executable path: {err}"); } }, Message::OpenItemLocation(entity_opt) => { let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return Task::batch(selected_paths.into_iter().filter_map(|path| { path.parent() .map(Path::to_path_buf) .map(|parent| self.open_tab(Location::Path(parent), true, Some(vec![path]))) })); } Message::OpenWithBrowse => match self.dialog_pages.pop_front() { Some(( DialogPage::OpenWith { mime, store_opt: Some(app), .. }, task, )) => { let url = format!("mime:///{mime}"); // TODO: Support multiple URLs if let Some(mut command) = app.command(&[&url]).and_then(|v| v.into_iter().next()) { if let Err(err) = spawn_detached(&mut command) { log::warn!("failed to open {:?} with {:?}: {}", url, app.id, err); } } else { log::warn!( "failed to open {:?} with {:?}: failed to get command", url, app.id ); } return task; } Some((dialog_page, task)) => { log::warn!("tried to open with browse from the wrong dialog"); return Task::batch([task, self.dialog_pages.push_front(dialog_page)]); } None => {} }, Message::OpenWithDialog(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data::(entity) && let Some(items) = tab.items_opt() { for item in items { if !item.selected { continue; } let Some(path) = item.path_opt() else { continue; }; return self.push_dialog( DialogPage::OpenWith { path: path.clone(), mime: item.mime.clone(), selected: 0, store_opt: "x-scheme-handler/mime" .parse::() .ok() .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), }, Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), ); } } } Message::OpenWithSelection(index) => { if let Some(DialogPage::OpenWith { selected, .. }) = self.dialog_pages.front_mut() { *selected = index; } } Message::Paste(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(path) = tab.location.path_opt() { let to = path.clone(); // Use cached clipboard data if available (needed for Wayland popups) match &self.clipboard_cache { ClipboardCache::Files(contents) => { if contents.paths.is_empty() { return iced::Task::future(tokio::time::sleep( std::time::Duration::from_millis(300), )) .discard() .chain( clipboard::read_data::().map( move |contents_opt| match contents_opt { Some(contents) => cosmic::action::app( Message::PasteContents(to.clone(), contents), ), None => { cosmic::action::app(Message::PasteImage(to.clone())) } }, ), ); } return self .update(Message::PasteContents(to.clone(), contents.clone())); } ClipboardCache::Image(contents) => { return self .update(Message::PasteImageContents(to.clone(), contents.clone())); } ClipboardCache::Video(contents) => { return self .update(Message::PasteVideoContents(to.clone(), contents.clone())); } ClipboardCache::Text(contents) => { return self .update(Message::PasteTextContents(to.clone(), contents.clone())); } ClipboardCache::Empty => { // Cache is empty, try reading from clipboard directly // (works when triggered from main window, e.g., Ctrl+V) return clipboard::read_data::().map( move |contents_opt| match contents_opt { Some(contents) => cosmic::action::app(Message::PasteContents( to.clone(), contents, )), None => cosmic::action::app(Message::PasteImage(to.clone())), }, ); } } } } Message::PasteContents(to, mut contents) => { contents.paths.retain(|p| *p != to); if !contents.paths.is_empty() { return match contents.kind { ClipboardKind::Copy => self.operation(Operation::Copy { paths: contents.paths, to, }), ClipboardKind::Cut { is_dnd } => self.operation(Operation::Move { paths: contents.paths, to, cross_device_copy: is_dnd, }), }; } } Message::PasteImage(to) => { return clipboard::read_data::().map(move |contents_opt| { match contents_opt { Some(contents) => { cosmic::action::app(Message::PasteImageContents(to.clone(), contents)) } // No image data in clipboard, try video data None => cosmic::action::app(Message::PasteVideo(to.clone())), } }); } Message::PasteImageContents(to, contents) => { let Some(extension) = contents.extension() else { log::warn!( "Ignoring paste: unknown image MIME type {:?}", contents.mime_type ); return Task::none(); }; // Generate unique filename for the pasted image let base_name = format!("{}.{}", fl!("pasted-image"), extension); let base_path = to.join(&base_name); let final_path = copy_unique_path(&base_path, &to); // Write image data to file match fs::write(&final_path, &contents.data) { Ok(_) => { log::info!("Pasted image saved to {:?}", final_path); } Err(err) => { log::error!("Failed to save pasted image: {}", err); } } } Message::PasteVideo(to) => { return clipboard::read_data::().map(move |contents_opt| { match contents_opt { Some(contents) => { cosmic::action::app(Message::PasteVideoContents(to.clone(), contents)) } // No video data in clipboard, try text data None => cosmic::action::app(Message::PasteText(to.clone())), } }); } Message::PasteVideoContents(to, contents) => { let Some(extension) = contents.extension() else { log::warn!( "Ignoring paste: unknown video MIME type {:?}", contents.mime_type ); return Task::none(); }; // Generate unique filename for the pasted video let base_name = format!("{}.{}", fl!("pasted-video"), extension); let base_path = to.join(&base_name); let final_path = copy_unique_path(&base_path, &to); // Write video data to file match fs::write(&final_path, &contents.data) { Ok(_) => { log::info!("Pasted video saved to {:?}", final_path); } Err(err) => { log::error!("Failed to save pasted video: {}", err); } } } Message::PasteText(to) => { return clipboard::read_data::().map(move |contents_opt| { match contents_opt { Some(contents) => { cosmic::action::app(Message::PasteTextContents(to.clone(), contents)) } None => cosmic::action::none(), } }); } Message::PasteTextContents(to, contents) => { // Generate unique filename for the pasted text let base_name = format!("{}.txt", fl!("pasted-text")); let base_path = to.join(&base_name); let final_path = copy_unique_path(&base_path, &to); // Write text data to file match fs::write(&final_path, &contents.data) { Ok(_) => { log::info!("Pasted text saved to {:?}", final_path); } Err(err) => { log::error!("Failed to save pasted text: {}", err); } } } Message::CheckClipboard => { // Check if clipboard has any paste-able content and cache it return clipboard::read_data::().map(|contents_opt| { match contents_opt { Some(contents) if contents.paths.is_empty() => cosmic::action::app( Message::RetryCheckClipboard(ClipboardCache::Files(contents)), ), Some(contents) => cosmic::action::app(Message::ClipboardCached( ClipboardCache::Files(contents), )), _ => cosmic::action::app(Message::CheckClipboardImage), } }); } Message::CheckClipboardImage => { return clipboard::read_data::().map(|contents_opt| { match contents_opt { Some(contents) => cosmic::action::app(Message::ClipboardCached( ClipboardCache::Image(contents), )), None => cosmic::action::app(Message::CheckClipboardVideo), } }); } Message::CheckClipboardVideo => { return clipboard::read_data::().map(|contents_opt| { match contents_opt { Some(contents) => cosmic::action::app(Message::ClipboardCached( ClipboardCache::Video(contents), )), None => cosmic::action::app(Message::CheckClipboardText), } }); } Message::CheckClipboardText => { return clipboard::read_data::().map(|contents_opt| { cosmic::action::app(Message::ClipboardCached(match contents_opt { Some(contents) => ClipboardCache::Text(contents), None => ClipboardCache::Empty, })) }); } Message::RetryCheckClipboard(cache) => { let mut cmds = Vec::new(); cmds.push(self.update(Message::ClipboardCached(cache))); cmds.push( iced::Task::future(tokio::time::sleep(Duration::from_millis(300))) .discard() .chain( clipboard::read_data::().map(|contents_opt| { match contents_opt { Some(contents) if !contents.paths.is_empty() => { cosmic::action::app(Message::ClipboardCached( ClipboardCache::Files(contents), )) } _ => cosmic::action::app(Message::CheckClipboardImage), } }), ), ); return Task::batch(cmds); } Message::ClipboardCached(cache) => { self.clipboard_cache = cache; } Message::PendingCancel(id) => { if let Some((_, controller)) = self.pending_operations.get(&id) { controller.cancel(); self.progress_operations.remove(&id); } } Message::PendingCancelAll => { for (id, (_, controller)) in &self.pending_operations { controller.cancel(); self.progress_operations.remove(id); } } Message::PendingComplete(id, op_sel) => { return self.handle_completed_operations(vec![(id, op_sel)]); } Message::PendingDismiss => { self.progress_operations.clear(); } Message::PendingError(id, err) => { return self.handle_operation_errors(vec![(id, err)]); } Message::PendingResults(completed, errors) => { return Task::batch(vec![ self.handle_completed_operations(completed), self.handle_operation_errors(errors), ]); } Message::PendingPause(id, pause) => { if let Some((_, controller)) = self.pending_operations.get(&id) { if pause { controller.pause(); } else { controller.unpause(); } } } Message::PendingPauseAll(pause) => { for (_, controller) in self.pending_operations.values() { if pause { controller.pause(); } else { controller.unpause(); } } } Message::PermanentlyDelete(entity_opt) => { let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); if !paths.is_empty() { return self.push_dialog( DialogPage::PermanentlyDelete { paths }, Some(PERMANENT_DELETE_BUTTON_ID.clone()), ); } } Message::Preview(entity_opt) => { match self.mode { Mode::App => { let show_details = !self.config.show_details; self.context_page = ContextPage::Preview(None, PreviewKind::Selected); self.core.window.show_context = show_details; return cosmic::task::message(Message::SetShowDetails(show_details)); } Mode::Desktop => { let preview_kind = { let mut selected_paths = self.selected_paths(entity_opt); match (selected_paths.next(), selected_paths.next()) { (Some(_), Some(_)) => Some(PreviewKind::Selected), (Some(path), None) => { Some(PreviewKind::Location(Location::Path(path))) } _ => None, } }; if let Some(preview_kind) = preview_kind { let mut settings = window::Settings { decorations: true, min_size: Some(Size::new(360.0, 180.0)), resizable: true, size: Size::new(480.0, 600.0), transparent: true, ..Default::default() }; #[cfg(target_os = "linux")] { // Use the dialog ID to make it float settings.platform_specific.application_id = "com.system76.CosmicFilesDialog".to_string(); } let (id, command) = window::open(settings); self.windows.insert( id, Window::new(WindowKind::Preview(entity_opt, preview_kind)), ); return Task::batch([ self.update_desktop(), // Force re-calculating of directory sizes command.map(|_id| cosmic::action::none()), ]); } } } } Message::RemoveFromRecents(entity_opt) => { let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); return self.operation(Operation::RemoveFromRecents { paths }); } Message::RescanRecents => { return self.rescan_recents(); } Message::RescanTrash => { // Update trash icon if empty/full let maybe_entity = self.nav_model.iter().find(|&entity| { self.nav_model .data::(entity) .is_some_and(|loc| matches!(loc, Location::Trash)) }); if let Some(entity) = maybe_entity { self.nav_model .icon_set(entity, icon::icon(Trash::icon_symbolic(16))); } return Task::batch([self.rescan_trash(), self.update_desktop()]); } Message::Rename(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(items) = tab.items_opt() { let selected: Box<[_]> = items .iter() .filter_map(|item| { if item.selected { item.path_opt().cloned() } else { None } }) .collect(); if !selected.is_empty() { //TODO: batch rename let mut last_name = String::new(); let tasks: Vec<_> = selected .into_iter() .filter_map(|path| { let parent = path.parent()?.to_path_buf(); let name = path.file_name()?.to_str()?.to_string(); let dir = path.is_dir(); last_name = name.clone(); Some(self.dialog_pages.push_back(DialogPage::RenameItem { from: path, parent, name, dir, })) }) .collect(); let tasks = tasks.into_iter().chain([ widget::text_input::focus(self.dialog_text_input.clone()), widget::text_input::select_until_last( self.dialog_text_input.clone(), &last_name, '.', ), ]); return Task::batch(tasks); } } } Message::ReplaceResult(replace_result) => { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() { match dialog_page { DialogPage::Replace { tx, .. } => { return Task::future(async move { let _ = tx.send(replace_result).await; cosmic::action::none() }); } other => { log::warn!("tried to send replace result to the wrong dialog"); return Task::batch([task, self.dialog_pages.push_front(other)]); } } } } Message::RestoreFromTrash(entity_opt) => { let mut trash_items = Vec::new(); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) && let Some(items) = tab.items_opt() { for item in items { if item.selected { if let ItemMetadata::Trash { entry, .. } = &item.metadata { trash_items.push(entry.clone()); } else { //TODO: error on trying to restore non-trash file? } } } } if !trash_items.is_empty() { return self.operation(Operation::Restore { items: trash_items }); } } Message::ScrollTab(scroll_speed) => { let entity = self.tab_model.active(); return self.update(Message::TabMessage( Some(entity), tab::Message::ScrollTab(f32::from(scroll_speed) / 10.0), )); } Message::SearchActivate => { let mut tasks = vec![self.close_context_menus()]; if self.search_get().is_none() { tasks.push(self.search_set_active(Some(String::new()))); } else { tasks.push(widget::text_input::focus(self.search_id.clone())); }; return Task::batch(tasks); } Message::SearchClear => { return Task::batch([self.close_context_menus(), self.search_set_active(None)]); } Message::SearchInput(input) => { return self.search_set_active(Some(input)); } Message::SetShowDetails(show_details) => { config_set!(show_details, show_details); return self.update_config(); } Message::SetShowRecents(show_recents) => { config_set!(show_recents, show_recents); return self.update_config(); } Message::SetTypeToSearch(type_to_search) => { config_set!(type_to_search, type_to_search); return self.update_config(); } Message::SystemThemeModeChange => { return self.update_config(); } Message::TabActivate(entity) => { let mut tasks = vec![self.close_context_menus()]; // Activate new tab self.tab_model.activate(entity); if let Some(tab) = self.tab_model.data::(entity) { { //Restore scroll //TODO: why do scrollers with different IDs get the same scroll position? let scroll = tab.scroll_opt.unwrap_or_default(); tasks.push(scrollable::scroll_to( tab.scrollable_id.clone(), AbsoluteOffset { x: Some(scroll.x), y: Some(scroll.y), }, )); } self.activate_nav_model_location(&tab.location.clone()); } tasks.push(self.update_title()); return Task::batch(tasks); } Message::TabNext => { let len = self.tab_model.len(); let pos = (self .tab_model .position(self.tab_model.active()) .expect("should always be at least one tab open") + 1) // Wraparound to 0 if i + 1 > num of tabs % len as u16; let entity = self.tab_model.entity_at(pos); if let Some(entity) = entity { return self.update(Message::TabActivate(entity)); } } Message::TabPrev => { let pos = self .tab_model .position(self.tab_model.active()) .expect("should always be at least one tab open") .checked_sub(1) // Subtraction underflow => last tab; i.e. it wraps around .unwrap_or_else(|| (self.tab_model.len() as u16).saturating_sub(1)); let entity = self.tab_model.entity_at(pos); if let Some(entity) = entity { return self.update(Message::TabActivate(entity)); } } Message::TabClose(entity_opt) => { let mut tasks = Vec::with_capacity(2); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); // If the last tab is closed, close the window // Otherwise, activate closest item if self.tab_model.len() == 1 { tasks.push(Task::future(async move { cosmic::action::app(Message::WindowClose) })); } else if let Some(position) = self.tab_model.position(entity) { let new_position = if position > 0 { position - 1 } else { position + 1 }; if let Some(new_entity) = self.tab_model.entity_at(new_position) { tasks.push(self.update(Message::TabActivate(new_entity))); } } // Remove item self.tab_model.remove(entity); tasks.push(self.update_watcher()); return Task::batch(tasks); } Message::TabConfig(config) => { if config != self.config.tab { config_set!(tab, config); return self.update_config(); } } Message::ToggleFoldersFirst => { let mut config = self.config.tab; config.folders_first = !config.folders_first; return self.update(Message::TabConfig(config)); } Message::ToggleShowHidden => { let mut config = self.config.tab; config.show_hidden = !config.show_hidden; return self.update(Message::TabConfig(config)); } Message::TabMessage(entity_opt, tab_message) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let tab_commands = match self.tab_model.data_mut::(entity) { Some(tab) => tab.update(tab_message, self.modifiers), _ => Vec::new(), }; let mut commands = Vec::new(); for tab_command in tab_commands { match tab_command { tab::Command::Action(action) => { commands.push(self.update(action.message(Some(entity)))); } tab::Command::AddNetworkDrive => { self.context_page = ContextPage::NetworkDrive; self.set_show_context(true); } tab::Command::AddToSidebar(path) => { let mut favorites = self.config.favorites.clone(); let favorite = Favorite::from_path(path); if !favorites.contains(&favorite) { favorites.push(favorite); } config_set!(favorites, favorites); commands.push(self.update_config()); } tab::Command::AutoScroll(scroll_speed) => { // converting an f32 to an i16 here by multiplying by 10 and casting to i16 // further resolution isn't necessary if let Some(scroll_speed_float) = scroll_speed { self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16); } else { self.auto_scroll_speed = None; } } tab::Command::ChangeLocation(tab_title, tab_path, selection_paths) => { self.activate_nav_model_location(&tab_path); self.tab_model.text_set(entity, tab_title); // clear the prefix selection buffer when changing location self.type_select_prefix.clear(); commands.push(Task::batch([ self.update_title(), self.update_watcher(), self.update_tab(entity, tab_path, selection_paths), ])); } tab::Command::ContextMenu(point_opt, parent_id) => { #[cfg(feature = "wayland")] if let Some(point) = point_opt { if crate::is_wayland() { // Open context menu use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ Anchor, Gravity, }; use cosmic::iced::runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; let window_id = WindowId::unique(); self.windows.insert( window_id, Window::new(WindowKind::ContextMenu( entity, widget::Id::unique(), )), ); commands.push(self.update(Message::CheckClipboard)); commands.push(self.update(Message::Surface( cosmic::surface::action::app_popup( move |app: &mut Self| -> SctkPopupSettings { let anchor_rect = Rectangle { x: point.x as i32, y: point.y as i32, width: 1, height: 1, }; let positioner = SctkPositioner { size: None, anchor_rect, anchor: Anchor::None, gravity: Gravity::BottomRight, reactive: true, ..Default::default() }; SctkPopupSettings { parent: parent_id.unwrap_or( app.core .main_window_id() .unwrap_or(WindowId::NONE), ), id: window_id, positioner, parent_size: None, grab: true, close_with_children: false, input_zone: None, } }, None, ), ))); } } else { // Destroy previous popup let mut window_ids = Vec::new(); for (window_id, window) in &self.windows { if let WindowKind::ContextMenu(e, _) = &window.kind && *e == entity { window_ids.push(*window_id); } } for window_id in window_ids { commands.push(self.update(Message::Surface( cosmic::surface::action::destroy_popup(window_id), ))); } } } tab::Command::Delete(paths) => commands.push(self.delete(paths)), tab::Command::DropFiles(to, from) => { commands.push(self.update(Message::PasteContents(to, from))); } tab::Command::EmptyTrash => { return self.push_dialog( DialogPage::EmptyTrash, Some(EMPTY_TRASH_BUTTON_ID.clone()), ); } #[cfg(feature = "desktop")] tab::Command::ExecEntryAction(entry, action) => { Self::exec_entry_action(&entry, action); } tab::Command::RunContextAction(action) => { let paths: Box<[_]> = self.selected_paths(Some(entity)).collect(); if let Some(preset) = self.config.context_actions.get(action) { if preset.confirm { commands.push(self.push_dialog( DialogPage::RunContextAction { action, paths }, Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), )); } else { context_action::run( &self.config.context_actions, action, &paths, ); } } else { log::warn!("invalid context action index `{action}`"); } } tab::Command::Iced(iced_command) => { commands.push(iced_command.0.map(move |x| { cosmic::action::app(Message::TabMessage(Some(entity), x)) })); } tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)), tab::Command::OpenInNewTab(path) => { commands.push(self.close_context_menus()); commands.push(self.open_tab(Location::Path(path), false, None)); } tab::Command::OpenInNewWindow(path) => match env::current_exe() { Ok(exe) => match process::Command::new(&exe).arg(path).spawn() { Ok(_child) => {} Err(err) => { log::error!("failed to execute {}: {}", exe.display(), err); } }, Err(err) => { log::error!("failed to get current executable path: {err}"); } }, tab::Command::OpenTrash => { //TODO: use handler for x-scheme-handler/trash and open trash:/// let mut command = process::Command::new("cosmic-files"); command.arg("--trash"); match spawn_detached(&mut command) { Ok(()) => {} Err(err) => { log::warn!("failed to run cosmic-files --trash: {err}"); } } } tab::Command::Preview(kind) => { self.context_page = ContextPage::Preview(Some(entity), kind); self.set_show_context(true); } tab::Command::SetOpenWith(mime, id) => { //TODO: this will block for a few ms, run in background? self.mime_app_cache.set_default(mime, id); } tab::Command::SetPermissions(path, mode) => { commands.push(self.operation(Operation::SetPermissions { path, mode })); } tab::Command::SetMultiplePermissions(permissions) => { commands.push( self.join_operations( permissions .into_iter() .map(|(path, mode)| Operation::SetPermissions { path, mode, }) .collect(), ), ); } tab::Command::WindowDrag => { if let Some(window_id) = self.core.main_window_id() { commands.push(window::drag(window_id)); } } tab::Command::WindowToggleMaximize => { if let Some(window_id) = self.core.main_window_id() { commands.push(window::toggle_maximize(window_id)); } } tab::Command::SetSort(location, heading_options, direction) => { let default_sort = tab::SORT_OPTION_FALLBACK .get(&location) .copied() .unwrap_or((HeadingOptions::Name, true)); let changed = if default_sort == (heading_options, direction) { self.state.sort_names.remove(&location).is_some() } else { // force reordering of inserted values so new settings are not dropped in the truncation step _ = self.state.sort_names.remove(&location); _ = self .state .sort_names .insert(location, (heading_options, direction)) .is_none_or(|old| old != (heading_options, direction)); const MAX_SORT_NAMES: usize = 999; // TODO potentially configurable limit on max size? if self.state.sort_names.len() > MAX_SORT_NAMES { // truncate is not a good fit because it drops the items at the end, which are newest... self.state.sort_names = self .state .sort_names .split_off(self.state.sort_names.len() - MAX_SORT_NAMES); } true }; if !self.must_save_sort_names & changed { self.must_save_sort_names = true; return cosmic::Task::future(async move { tokio::time::sleep(Duration::from_secs(1)).await; cosmic::action::app(Message::SaveSortNames) }); } } } } return Task::batch(commands); } Message::TabNew => { let active = self.tab_model.active(); let location = match self.tab_model.data::(active) { Some(tab) => tab.location.clone(), None => Location::Path(home_dir()), }; return self.open_tab(location, true, None); } Message::TabRescan(entity, mut location, parent_item_opt, items, selection_paths) => { location = location.normalize(); if let Some(tab) = self.tab_model.data_mut::(entity) { tab.location = tab.location.normalize(); if location == tab.location { tab.parent_item_opt = parent_item_opt; tab.set_items(items); let location_str = location.to_string(); let sort = self .state .sort_names .get(&location_str) .or_else(|| SORT_OPTION_FALLBACK.get(&location_str)) .unwrap_or(&(HeadingOptions::Name, true)); tab.sort_name = sort.0; tab.sort_direction = sort.1; let mut tasks = Vec::with_capacity(2); if let Some(selection_paths) = selection_paths { tab.select_paths(selection_paths); // Ensure selected path is scrolled to after redraw tasks.push(Task::done(cosmic::action::app(Message::TabMessage( Some(entity), tab::Message::ScrollToFocused, )))); } tasks.push(clipboard::read_data::().map(|p| { cosmic::action::app(Message::CutPaths(match p { Some(s) => match s.kind { ClipboardKind::Copy => Vec::new(), ClipboardKind::Cut { .. } => s.paths, }, None => Vec::new(), })) })); return Task::batch(tasks); } } } Message::TabView(entity_opt, view) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { if matches!(tab.mode, tab::Mode::Desktop) { return Task::none(); } tab.config.view = view; } let mut config = self.config.tab; config.view = view; return self.update(Message::TabConfig(config)); } Message::CutPaths(paths) => { if let Some(tab) = self.tab_model.active_data_mut::() { tab.refresh_cut(&paths); } } Message::TimeConfigChange(time_config) => { self.config.tab.military_time = time_config.military_time; return self.update_config(); } Message::ToggleContextPage(context_page) => { //TODO: ensure context menus are closed if self.context_page == context_page || matches!(self.context_page, ContextPage::Preview(_, _)) { self.set_show_context(!self.core.window.show_context); } else { self.set_show_context(true); } self.context_page = context_page; // Preview status is preserved across restarts if matches!(self.context_page, ContextPage::Preview(_, _)) { return cosmic::task::message(cosmic::action::app(Message::SetShowDetails( self.core.window.show_context, ))); } } Message::Undo(_id) => { // TODO: undo } Message::UndoTrash(id, recently_trashed) => { self.toasts.remove(id); let mut paths = Vec::with_capacity(recently_trashed.len()); let icon_sizes = self.config.tab.icon_sizes; return cosmic::task::future(async move { match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes)) .await { Ok((_parent_item_opt, items)) => { for path in &*recently_trashed { for item in &items { if let ItemMetadata::Trash { ref entry, .. } = item.metadata { let original_path = entry.original_path(); if &original_path == path { paths.push(entry.clone()); } } } } } Err(err) => { log::warn!("failed to rescan: {err}"); } } Message::UndoTrashStart(paths) }); } Message::UndoTrashStart(items) => { return self.operation(Operation::Restore { items }); } Message::WindowClose => { if let Some(window_id) = self.core.main_window_id() { self.core.set_main_window_id(None); return Task::batch([ window::close(window_id), Task::future(async move { cosmic::action::app(Message::MaybeExit) }), ]); } } Message::WindowCloseRequested(id) => { self.remove_window(&id); } Message::WindowMaximize(id, maximized) => { return window::maximize(id, maximized); } Message::WindowNew => match env::current_exe() { Ok(exe) => { // initialize command to spawn another instance of this application let mut command = process::Command::new(&exe); // make the new window open at the same location as the currently active tab by // passing respective command line arguments let entity = self.tab_model.active(); let active_tab_location = self.tab_model.data::(entity).map(|tab| &tab.location); match active_tab_location { Some( Location::Desktop(path, ..) | Location::Path(path) | Location::Search(SearchLocation::Path(path), ..), ) => { command.arg(path); } Some(Location::Network(uri, ..)) => { command.arg(uri); } Some(Location::Recents | Location::Search(SearchLocation::Recents, ..)) => { command.arg("--recents"); } Some(Location::Trash | Location::Search(SearchLocation::Trash, ..)) => { command.arg("--trash"); } None => {} }; // spawn the new window match command.spawn() { Ok(_child) => {} Err(err) => { log::error!("failed to execute {}: {}", exe.display(), err); } } } Err(err) => { log::error!("failed to get current executable path: {err}"); } }, Message::ZoomDefault(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { zoom_to_default(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } Message::ZoomIn(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { zoom_in_view(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } Message::ZoomOut(entity_opt) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let mut config = self.config.tab; if let Some(tab) = self.tab_model.data::(entity) { zoom_out_view(tab.config.view, &mut config.icon_sizes); } return self.update(Message::TabConfig(config)); } Message::DndEnterNav(entity) => { if let Some(location) = self.nav_model.data::(entity) { self.nav_dnd_hover = Some((location.clone(), Instant::now())); let location = location.clone(); return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| { cosmic::Action::App(Message::DndHoverLocTimeout(location.clone())) }); } } Message::DndExitNav => { self.nav_dnd_hover = None; } Message::DndDropNav(entity, data, action) => { self.nav_dnd_hover = None; if let Some((location, data)) = self.nav_model.data::(entity).zip(data) { let kind = match action { DndAction::Move => ClipboardKind::Cut { is_dnd: true }, _ => ClipboardKind::Copy, }; let ret = match location { Location::Path(p) => self.update(Message::PasteContents( p.clone(), ClipboardPaste { kind, paths: data.paths, }, )), Location::Trash if matches!(action, DndAction::Move) => { self.delete(data.paths) } _ => { log::warn!("Copy to trash is not supported."); Task::none() } }; return ret; } } Message::DndHoverLocTimeout(location) => { if self .nav_dnd_hover .as_ref() .is_some_and(|(loc, i)| *loc == location && i.elapsed() >= HOVER_DURATION) { self.nav_dnd_hover = None; let entity = self.tab_model.active(); let title_opt = match self.tab_model.data_mut::(entity) { Some(tab) => { tab.change_location(&location, None); Some(tab.title()) } None => None, }; if let Some(title) = title_opt { self.tab_model.text_set(entity, title); return Task::batch([ self.update_title(), self.update_watcher(), self.update_tab(entity, location, None), ]); } } } Message::DndEnterTab(entity, mimes) => { if mimes.iter().all(|m| m.as_str() != "x-cosmic-files/tab-dnd") { self.tab_dnd_hover = Some((entity, Instant::now())); return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| { cosmic::Action::App(Message::DndHoverTabTimeout(entity)) }); } } Message::DndExitTab => { self.nav_dnd_hover = None; } Message::DndDropTab(entity, data, action) => { self.nav_dnd_hover = None; if let Some((tab, data)) = self.tab_model.data::(entity).zip(data) { let kind = match action { DndAction::Move => ClipboardKind::Cut { is_dnd: true }, _ => ClipboardKind::Copy, }; let ret = match &tab.location { Location::Trash if matches!(action, DndAction::Move) => { self.delete(data.paths) } _ => { if let Some(path) = tab.location.path_opt() { self.update(Message::PasteContents( path.clone(), ClipboardPaste { kind, paths: data.paths, }, )) } else { log::warn!("{:?} to {:?} is not supported.", action, tab.location); Task::none() } } }; return ret; } } Message::DndHoverTabTimeout(entity) => { if self .tab_dnd_hover .as_ref() .is_some_and(|(e, i)| *e == entity && i.elapsed() >= HOVER_DURATION) { self.tab_dnd_hover = None; return self.update(Message::TabActivate(entity)); } } Message::NavBarClose(entity) => { if let Some(data) = self.nav_model.data::(entity) && let Some(mounter) = MOUNTERS.get(&data.0) { return mounter .unmount(data.1.clone()) .map(|()| cosmic::action::none()); } } Message::NavBarContext(entity) => { self.nav_bar_context_id = entity; let tab_entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data_mut::(tab_entity) { // Close location editing if enabled tab.edit_location = None; // Close other context menus. tab.location_context_menu_index = None; return Task::done(cosmic::Action::App(Message::TabMessage( Some(tab_entity), tab::Message::ContextMenu(None, None), ))); } } Message::NavMenuAction(action) => match action { NavMenuAction::ClearRecents => match recently_used_xbel::clear_recently_used() { Ok(()) => {} Err(err) => { log::warn!("failed to clear recents history: {}", err); } }, NavMenuAction::EmptyTrash => { return self .push_dialog(DialogPage::EmptyTrash, Some(EMPTY_TRASH_BUTTON_ID.clone())); } NavMenuAction::Open(entity) => { if let Some(path) = self .nav_model .data::(entity) .and_then(Location::path_opt) .cloned() { return self.open_file(&[path]); } } NavMenuAction::OpenWith(entity) => { if let Some(path) = self .nav_model .data::(entity) .and_then(Location::path_opt) .cloned() { match tab::item_from_path(&path, IconSizes::default()) { Ok(item) => { return self.push_dialog( DialogPage::OpenWith { path, mime: item.mime, selected: 0, store_opt: "x-scheme-handler/mime" .parse::() .ok() .and_then(|mime| { self.mime_app_cache.get(&mime).first().cloned() }), }, None, ); } Err(err) => { log::warn!( "failed to get item for path {}: {}", path.display(), err ); } } } } NavMenuAction::RunContextAction(entity, action) => { if let Some(path) = self .nav_model .data::(entity) .and_then(Location::path_opt) .cloned() { let paths = vec![path]; if let Some(preset) = self.config.context_actions.get(action) { if preset.confirm { return self.push_dialog( DialogPage::RunContextAction { action, paths: paths.into_boxed_slice(), }, Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), ); } context_action::run(&self.config.context_actions, action, &paths); } else { log::warn!("invalid context action index `{action}`"); } } } NavMenuAction::OpenInNewTab(entity) => { let open_task = match self.nav_model.data::(entity) { Some(Location::Network(uri, display_name, path)) => self.open_tab( Location::Network(uri.clone(), display_name.clone(), path.clone()), false, None, ), Some(Location::Path(path)) => { self.open_tab(Location::Path(path.clone()), false, None) } Some(Location::Recents) => self.open_tab(Location::Recents, false, None), Some(Location::Trash) => self.open_tab(Location::Trash, false, None), _ => Task::none(), }; return Task::batch([self.close_context_menus(), open_task]); } // Open the selected path in a new cosmic-files window. NavMenuAction::OpenInNewWindow(entity) => 'open_in_new_window: { if let Some(location) = self.nav_model.data::(entity) { match env::current_exe() { Ok(exe) => { let mut command = process::Command::new(&exe); match location { Location::Path(path) => { command.arg(path); } Location::Trash => { command.arg("--trash"); } Location::Network(uri, _, Some(_)) => { command.arg(uri); } Location::Network(..) => { command.arg("--network"); } Location::Recents => { command.arg("--recents"); } _ => { log::error!( "unsupported location for open in new window: {location:?}" ); break 'open_in_new_window; } } match command.spawn() { Ok(_child) => {} Err(err) => { log::error!("failed to execute {}: {}", exe.display(), err); } } } Err(err) => { log::error!("failed to get current executable path: {err}"); } } } } NavMenuAction::Preview(entity) => { if let Some(path) = self .nav_model .data::(entity) .and_then(Location::path_opt) { match tab::item_from_path(path, IconSizes::default()) { Ok(item) => { self.context_page = ContextPage::Preview( None, PreviewKind::Custom(PreviewItem(Box::new(item))), ); self.set_show_context(true); } Err(err) => { log::warn!( "failed to get item from path {}: {}", path.display(), err ); } } } } NavMenuAction::RemoveFromSidebar(entity) => { if let Some(FavoriteIndex(favorite_i)) = self.nav_model.data::(entity) { let mut favorites = self.config.favorites.clone(); favorites.remove(*favorite_i); config_set!(favorites, favorites); return self.update_config(); } } }, Message::Recents => { if self.config.show_recents { return self.open_tab(Location::Recents, false, None); } } #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Message::OutputEvent(output_event, output) => { match output_event { OutputEvent::Created(output_info_opt) => { let output_id = output.id(); log::info!("output {output_id}: created"); let surface_id = WindowId::unique(); if let Some(old_surface_id) = self.surface_ids.insert(output.clone(), surface_id) { //TODO: remove old surface? log::warn!( "output {output_id}: already had surface ID {old_surface_id:?}" ); } let display = match output_info_opt { Some(output_info) => match output_info.name { Some(output_name) => { self.surface_names.insert(surface_id, output_name.clone()); output_name } None => { log::warn!("output {output_id}: no output name"); String::new() } }, None => { log::warn!("output {output_id}: no output info"); String::new() } }; let (entity, command) = self.open_tab_entity( Location::Desktop(crate::desktop_dir(), display, self.config.desktop), false, None, widget::Id::unique(), Some(surface_id), ); self.windows .insert(surface_id, Window::new(WindowKind::Desktop(entity))); return Task::batch([ command, get_layer_surface(SctkLayerSurfaceSettings { id: surface_id, layer: Layer::Bottom, keyboard_interactivity: KeyboardInteractivity::OnDemand, input_zone: None, anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, output: IcedOutput::Output(output), namespace: "cosmic-files-applet".into(), size: Some((None, None)), margin: IcedMargin { top: 0, bottom: 0, left: 0, right: 0, }, exclusive_zone: 0, size_limits: Limits::NONE.min_width(1.0).min_height(1.0), }), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] overlap_notify(surface_id, true), ]); } OutputEvent::Removed => { log::info!("output {}: removed", output.id()); match self.surface_ids.remove(&output) { Some(surface_id) => { self.remove_window(&surface_id); self.surface_names.remove(&surface_id); return destroy_layer_surface(surface_id); } None => { log::warn!("output {}: no surface found", output.id()); } } } OutputEvent::InfoUpdate(_output_info) => { log::info!("output {}: info update", output.id()); } } } Message::Cosmic(cosmic) => { // Forward cosmic messages return Task::perform(async move { cosmic }, cosmic::action::cosmic); } Message::None => {} #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Message::Overlap(w_id, overlap_notify_event) => match overlap_notify_event { OverlapNotifyEvent::OverlapLayerAdd { identifier, namespace, logical_rect, exclusive, .. } => { if exclusive > 0 || namespace == "Dock" || namespace == "Panel" { self.overlap.insert(identifier, (w_id, logical_rect)); self.handle_overlap(); } } OverlapNotifyEvent::OverlapLayerRemove { identifier } => { self.overlap.remove(&identifier); self.handle_overlap(); } _ => {} }, Message::Size(window_id, size) => { if self.core.main_window_id() == Some(window_id) { self.size = Some(size); } else { #[cfg(all(feature = "wayland", feature = "desktop-applet"))] self.layer_sizes.insert(window_id, size); } } Message::Eject => { #[cfg(feature = "gvfs")] { let mut paths = self.selected_paths(None); if let Some(p) = paths.next() { { for (k, mounter_items) in &self.mounter_items { if let Some(mounter) = MOUNTERS.get(k) && let Some(item) = mounter_items .iter() .find(|&item| item.path().is_some_and(|path| path == p)) { return mounter .unmount(item.clone()) .map(|()| cosmic::action::none()); } } } } } } #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Message::Focused(id) => { if let Some(w) = self.windows.get(&id) { match &w.kind { WindowKind::Desktop(entity) => self.tab_model.activate(*entity), _ => {} }; } // Check clipboard when window gains focus // HACK: Wait a moment for the data to be available. return cosmic::task::future(async { _ = tokio::time::sleep(Duration::from_millis(300)).await; cosmic::action::app(Message::CheckClipboard) }); } Message::Surface(action) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(action), )); } Message::SaveSortNames => { self.must_save_sort_names = false; if let Some(state_handler) = self.state_handler.as_ref() && let Err(err) = state_handler .set::<&FxOrderMap>( "sort_names", &self.state.sort_names, ) { log::warn!("Failed to save sort names: {err:?}"); } } Message::NetworkDriveOpenEntityAfterMount { entity } => { return self.on_nav_select(entity); } Message::NetworkDriveOpenTabAfterMount { location } => { return self.open_tab(location, false, None); } Message::ReorderTab(ReorderEvent { dragged, target, position, }) => { _ = self.tab_model.reorder(dragged, target, position); } } Task::none() } fn context_drawer(&self) -> Option> { if !self.core.window.show_context { return None; } Some(match &self.context_page { ContextPage::About => context_drawer::about( &self.about, |url| Message::LaunchUrl(url.to_string()), Message::ToggleContextPage(ContextPage::About), ), ContextPage::EditHistory => context_drawer::context_drawer( self.edit_history(), Message::ToggleContextPage(ContextPage::EditHistory), ) .title(fl!("edit-history")), ContextPage::NetworkDrive => { let mut text_input = widget::text_input(fl!("enter-server-address"), &self.network_drive_input); let button = if self.network_drive_connecting.is_some() { widget::button::standard(fl!("connecting")) } else { text_input = text_input .on_input(Message::NetworkDriveInput) .on_submit(|_| Message::NetworkDriveSubmit); widget::button::standard(fl!("connect")).on_press(Message::NetworkDriveSubmit) }; context_drawer::context_drawer( self.network_drive(), Message::ToggleContextPage(ContextPage::NetworkDrive), ) .title(fl!("add-network-drive")) .header(text_input) .footer(widget::row::with_children([ widget::space::horizontal().into(), button.into(), ])) } ContextPage::Preview(entity_opt, kind) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let actions = self .tab_model .data::(entity) .and_then(|tab| { let mut selected = tab.items_opt()?.iter().filter(|item| item.selected); match (selected.next(), selected.next()) { // Exactly one item (Some(item), None) => Some( item.preview_actions() .map(move |x| Message::TabMessage(Some(entity), x)), ), // Zero or more than one item _ => None, } }) .unwrap_or_else(|| widget::space::horizontal().into()); context_drawer::context_drawer( self.preview(entity_opt, kind, true) .map(move |x| Message::TabMessage(Some(entity), x)), Message::ToggleContextPage(ContextPage::Preview(Some(entity), kind.clone())), ) .actions(actions) } ContextPage::Settings => context_drawer::context_drawer( self.settings(), Message::ToggleContextPage(ContextPage::Settings), ) .title(fl!("settings")), }) } fn dialog(&self) -> Option> { //TODO: should gallery view just be a dialog? let entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data::(entity) && tab.gallery { return Some( tab.gallery_view() .map(move |x| Message::TabMessage(Some(entity), x)), ); } let dialog_page = self.dialog_pages.front()?; let cosmic_theme::Spacing { space_xxs, space_s, .. } = theme::spacing(); let dialog = match dialog_page { DialogPage::Compress { paths, to, name, archive_type, password, } => { let mut dialog = widget::dialog().title(fl!("create-archive")); let complete_maybe = if name.is_empty() { None } else if name == "." || name == ".." { dialog = dialog.tertiary_action(widget::text::body(fl!( "name-invalid", filename = name.as_str() ))); None } else if name.contains('/') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); None } else { let extension = archive_type.extension(); let name = format!("{name}{extension}"); let path = to.join(&name); if path.exists() { dialog = dialog.tertiary_action(widget::text::body(fl!("file-already-exists"))); None } else { if name.starts_with('.') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); } Some(Message::DialogComplete) } }; let archive_types = ArchiveType::all(); let selected = archive_types.iter().position(|&x| x == *archive_type); dialog = dialog .primary_action( widget::button::suggested(fl!("create")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( widget::column::with_children([ widget::text::body(fl!("file-name")).into(), widget::row::with_children([ widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) .on_input(move |name| { Message::DialogUpdate(DialogPage::Compress { paths: paths.clone(), to: to.clone(), name, archive_type: *archive_type, password: password.clone(), }) }) .on_submit_maybe( complete_maybe.clone().map(|maybe| move |_| maybe.clone()), ) .into(), Element::from(widget::dropdown( archive_types, selected, move |index| index, )) .map(|index| { Message::DialogUpdate(DialogPage::Compress { paths: paths.clone(), to: to.clone(), name: name.clone(), archive_type: archive_types[index], password: password.clone(), }) }), ]) .align_y(Alignment::Center) .spacing(space_xxs) .into(), ]) .spacing(space_xxs), ); if *archive_type == ArchiveType::Zip { let password_unwrapped = password.clone().unwrap_or_default(); dialog = dialog.control(widget::column::with_children([ widget::text::body(fl!("password")).into(), widget::text_input("", password_unwrapped) .password() .on_input(move |password_unwrapped| { Message::DialogUpdate(DialogPage::Compress { paths: paths.clone(), to: to.clone(), name: name.clone(), archive_type: *archive_type, password: Some(password_unwrapped), }) }) .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) .into(), ])); } dialog } DialogPage::EmptyTrash => widget::dialog() .title(fl!("empty-trash-title")) .body(fl!("empty-trash-warning")) .primary_action( widget::button::suggested(fl!("empty-trash")) .on_press(Message::DialogComplete) .id(EMPTY_TRASH_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ), DialogPage::FailedOperation(id) => { //TODO: try next dialog page (making sure index is used by Dialog messages)? let (operation, _, err) = self.failed_operations.get(id)?; //TODO: nice description of error widget::dialog() .title("Failed operation") .body(format!("{operation:#?}\n{err}")) .icon(icon::from_name("dialog-error").size(64)) //TODO: retry action .primary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } DialogPage::FailedOperations(ids) => { let errors: Vec = ids .iter() .filter_map(|id| match self.failed_operations.get(id) { Some((operation, _, err)) => Some(format!("{operation:#?}\n{err}")), _ => None, }) .collect(); //TODO: nice description of error widget::dialog() .title("Failed operations") .body(errors.join("\n\n")) .icon(icon::from_name("dialog-error").size(64)) //TODO: retry action .primary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } DialogPage::ExtractPassword { id, password } => widget::dialog() .title(fl!("extract-password-required")) .icon(icon::from_name("dialog-error").size(64)) .control( widget::text_input("", password) .password() .on_input(move |password| { Message::DialogUpdate(DialogPage::ExtractPassword { id: *id, password }) }) .on_submit(|_| Message::DialogComplete) .id(self.dialog_text_input.clone()), ) .primary_action( widget::button::suggested(fl!("extract-here")) .on_press(Message::DialogComplete), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ), DialogPage::MountError { mounter_key: _, item: _, error, } => widget::dialog() .title(fl!("mount-error")) .body(error) .icon(icon::from_name("dialog-error").size(64)) .primary_action( widget::button::standard(fl!("try-again")) .on_press(Message::DialogComplete) .id(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ), DialogPage::NetworkAuth { mounter_key, uri, auth, auth_tx, } => { //TODO: use URI! let mut controls = widget::column::with_capacity(4); let mut id_assigned = false; if let Some(username) = &auth.username_opt { //TODO: what should submit do? let mut input = widget::text_input(fl!("username"), username) .on_input(move |value| { Message::DialogUpdate(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), auth: MounterAuth { username_opt: Some(value), ..auth.clone() }, auth_tx: auth_tx.clone(), }) }) .on_submit(|_| Message::DialogComplete); if !id_assigned { input = input.id(self.dialog_text_input.clone()); id_assigned = true; } controls = controls.push(input); } if let Some(domain) = &auth.domain_opt { //TODO: what should submit do? let mut input = widget::text_input(fl!("domain"), domain) .on_input(move |value| { Message::DialogUpdate(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), auth: MounterAuth { domain_opt: Some(value), ..auth.clone() }, auth_tx: auth_tx.clone(), }) }) .on_submit(|_| Message::DialogComplete); if !id_assigned { input = input.id(self.dialog_text_input.clone()); id_assigned = true; } controls = controls.push(input); } if let Some(password) = &auth.password_opt { //TODO: what should submit do? //TODO: button for showing password let mut input = widget::secure_input(fl!("password"), password, None, true) .on_input(move |value| { Message::DialogUpdate(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), auth: MounterAuth { password_opt: Some(value), ..auth.clone() }, auth_tx: auth_tx.clone(), }) }) .on_submit(|_| Message::DialogComplete); if !id_assigned { input = input.id(self.dialog_text_input.clone()); } controls = controls.push(input); } if let Some(remember) = &auth.remember_opt { //TODO: what should submit do? //TODO: button for showing password controls = controls.push( widget::checkbox(*remember) .label(fl!("remember-password")) .on_toggle(move |value| { Message::DialogUpdate(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), auth: MounterAuth { remember_opt: Some(value), ..auth.clone() }, auth_tx: auth_tx.clone(), }) }), ); } let mut parts = auth.message.splitn(2, '\n'); let title = parts.next().unwrap_or_default(); let body = parts.next().unwrap_or_default(); let mut widget = widget::dialog() .title(title) .body(body) .control(controls.spacing(space_s)) .primary_action( widget::button::suggested(fl!("connect")).on_press(Message::DialogComplete), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ); if let Some(_anonymous) = &auth.anonymous_opt { widget = widget.tertiary_action( widget::button::text(fl!("connect-anonymously")).on_press( Message::DialogUpdateComplete(DialogPage::NetworkAuth { mounter_key: *mounter_key, uri: uri.clone(), auth: MounterAuth { anonymous_opt: Some(true), ..auth.clone() }, auth_tx: auth_tx.clone(), }), ), ); } widget } DialogPage::NetworkError { mounter_key: _, uri: _, error, } => widget::dialog() .title(fl!("network-drive-error")) .body(error) .icon(icon::from_name("dialog-error").size(64)) .primary_action( widget::button::standard(fl!("try-again")).on_press(Message::DialogComplete), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ), DialogPage::NewItem { parent, name, dir } => { let mut dialog = widget::dialog().title(if *dir { fl!("create-new-folder") } else { fl!("create-new-file") }); let complete_maybe = if name.is_empty() { None } else if name == "." || name == ".." { dialog = dialog.tertiary_action(widget::text::body(fl!( "name-invalid", filename = name.as_str() ))); None } else if name.contains('/') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); None } else { let path = parent.join(name); if path.exists() { if path.is_dir() { dialog = dialog .tertiary_action(widget::text::body(fl!("folder-already-exists"))); } else { dialog = dialog .tertiary_action(widget::text::body(fl!("file-already-exists"))); } None } else { if name.starts_with('.') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); } Some(Message::DialogComplete) } }; dialog .primary_action( widget::button::suggested(fl!("save")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( widget::column::with_children([ widget::text::body(if *dir { fl!("folder-name") } else { fl!("file-name") }) .into(), widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) .on_input(move |name| { Message::DialogUpdate(DialogPage::NewItem { parent: parent.clone(), name, dir: *dir, }) }) .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) .into(), ]) .spacing(space_xxs), ) } DialogPage::RunContextAction { action, paths } => { let name = self .config .context_actions .get(*action) .map_or_else(|| fl!("context-action"), |preset| preset.name.clone()); widget::dialog() .title(fl!("context-action-confirm-title", name = name)) .body(fl!("context-action-confirm-warning", items = paths.len())) .icon(icon::from_name("dialog-error").size(64)) .primary_action( widget::button::suggested(fl!("run")) .on_press(Message::DialogComplete) .id(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } DialogPage::OpenWith { path, mime, selected, store_opt, .. } => { let name = match path.file_name() { Some(file_name) => file_name.to_str(), None => path.as_os_str().to_str(), }; let mut column = widget::list_column(); let available_apps = self.get_apps_for_mime(mime); let item_height = 32.0; let mut displayed_default = false; let mut last_kind = MimeAppMatch::Exact; for (i, &(app, kind)) in available_apps.iter().enumerate() { if kind != last_kind { match kind { MimeAppMatch::Related => { column = column.add(widget::text::heading(fl!("related-apps"))); } MimeAppMatch::Other => { column = column.add(widget::text::heading(fl!("other-apps"))); } _ => {} } last_kind = kind; } column = column.add( widget::mouse_area( widget::button::custom( widget::row::with_children([ icon(app.icon.clone()).size(32).into(), if app.is_default && !displayed_default { displayed_default = true; widget::text::body(fl!( "default-app", name = Some(app.name.as_str()) )) .into() } else { widget::text::body(app.name.clone()).into() }, widget::space::horizontal().into(), if *selected == i { icon::from_name("checkbox-checked-symbolic").size(16).into() } else { widget::space::horizontal() .width(Length::Fixed(16.0)) .into() }, ]) .spacing(space_s) .height(Length::Fixed(item_height)) .align_y(Alignment::Center), ) .width(Length::Fill) .class(theme::Button::MenuItem) .force_enabled(true), ) .on_press(Message::OpenWithSelection(i)) .on_double_press(Message::DialogComplete), ); } let mut dialog = widget::dialog() .title(fl!("open-with-title", name = name)) .primary_action( widget::button::suggested(fl!("open")) .on_press(Message::DialogComplete) .id(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control(widget::scrollable(column).height({ let max_size = self .size .map_or(480.0, |size| (size.height - 256.0).min(480.0)); // (32 (item_height) + 5.0 (custom button padding)) + (space_xxs (list item spacing) * 2) let scrollable_height = available_apps.len() as f32 * f32::from(space_xxs).mul_add(2.0, item_height + 5.0); if scrollable_height > max_size { Length::Fixed(max_size) } else { Length::Shrink } })); if let Some(app) = store_opt { dialog = dialog.tertiary_action( widget::button::text(fl!("browse-store", store = app.name.as_str())) .on_press(Message::OpenWithBrowse), ); } dialog } DialogPage::PermanentlyDelete { paths } => { let target = if paths.len() == 1 { format!( "\"{}\"", paths[0].file_name().map_or_else( || paths[0].to_string_lossy(), std::ffi::OsStr::to_string_lossy ) ) } else { fl!("selected-items", items = paths.len()) }; widget::dialog() .title(fl!("permanently-delete-question")) .primary_action( widget::button::destructive(fl!("delete")) .on_press(Message::DialogComplete) .id(PERMANENT_DELETE_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control(widget::text(fl!( "permanently-delete-warning", target = target ))) } DialogPage::DeleteTrash { items } => { let target = if items.len() == 1 { format!("\"{}\"", items[0].name.to_string_lossy()) } else { fl!("selected-items", items = items.len()) }; widget::dialog() .title(fl!("permanently-delete-question")) .primary_action( widget::button::destructive(fl!("delete")) .on_press(Message::DialogComplete) .id(DELETE_TRASH_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control(widget::text(fl!( "permanently-delete-warning", target = target ))) } DialogPage::RenameItem { from, parent, name, dir, } => { //TODO: combine logic with NewItem let mut dialog = widget::dialog().title(if *dir { fl!("rename-folder") } else { fl!("rename-file") }); let complete_maybe = if name.is_empty() { None } else if name == "." || name == ".." { dialog = dialog.tertiary_action(widget::text::body(fl!( "name-invalid", filename = name.as_str() ))); None } else if name.contains('/') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); None } else { let path = parent.join(name); if *from != path && path.exists() { if path.is_dir() { dialog = dialog .tertiary_action(widget::text::body(fl!("folder-already-exists"))); } else { dialog = dialog .tertiary_action(widget::text::body(fl!("file-already-exists"))); } None } else { if name.starts_with('.') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); } Some(Message::DialogComplete) } }; dialog .primary_action( widget::button::suggested(fl!("rename")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( widget::column::with_children([ widget::text::body(if *dir { fl!("folder-name") } else { fl!("file-name") }) .into(), widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) .double_click_select_delimiter('.') .on_input(move |name| { Message::DialogUpdate(DialogPage::RenameItem { from: from.clone(), parent: parent.clone(), name, dir: *dir, }) }) .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) .into(), ]) .spacing(space_xxs), ) } DialogPage::Replace { from, to, multiple, apply_to_all, conflict_count, tx, } => { let military_time = self.config.tab.military_time; let dialog = widget::dialog() .title(fl!("replace-title", filename = to.name.as_str())) .body(fl!("replace-warning-operation")) .control( to.replace_view(fl!("original-file"), military_time) .map(|x| Message::TabMessage(None, x)), ) .control( from.replace_view(fl!("replace-with"), military_time) .map(|x| Message::TabMessage(None, x)), ) .primary_action( widget::button::suggested(fl!("replace")) .on_press(Message::ReplaceResult(ReplaceResult::Replace( *apply_to_all, ))) .id(REPLACE_BUTTON_ID.clone()), ); if *multiple { dialog .control( widget::checkbox(*apply_to_all) .label(format!("{} ({})", fl!("apply-to-all"), *conflict_count)) .on_toggle(|apply_to_all| { Message::DialogUpdate(DialogPage::Replace { from: from.clone(), to: to.clone(), multiple: *multiple, apply_to_all, conflict_count: *conflict_count, tx: tx.clone(), }) }), ) .secondary_action( widget::button::standard(fl!("skip")).on_press(Message::ReplaceResult( ReplaceResult::Skip(*apply_to_all), )), ) .tertiary_action( widget::button::text(fl!("cancel")) .on_press(Message::ReplaceResult(ReplaceResult::Cancel)), ) } else { dialog .secondary_action( widget::button::standard(fl!("cancel")) .on_press(Message::ReplaceResult(ReplaceResult::Cancel)), ) .tertiary_action( widget::button::text(fl!("keep-both")) .on_press(Message::ReplaceResult(ReplaceResult::KeepBoth)), ) } } DialogPage::SetExecutableAndLaunch { path } => { let name = match path.file_name() { Some(file_name) => file_name.to_str(), None => path.as_os_str().to_str(), }; widget::dialog() .title(fl!("set-executable-and-launch")) .primary_action( widget::button::text(fl!("set-and-launch")) .class(theme::Button::Suggested) .on_press(Message::DialogComplete) .id(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()), ) .secondary_action( widget::button::text(fl!("cancel")) .class(theme::Button::Standard) .on_press(Message::DialogCancel), ) .control(widget::text::text(fl!( "set-executable-and-launch-description", name = name ))) } DialogPage::FavoritePathError { path, .. } => widget::dialog() .title(fl!("favorite-path-error")) .body(fl!( "favorite-path-error-description", path = path.as_os_str().to_str() )) .icon(icon::from_name("dialog-error").size(64)) .primary_action( widget::button::destructive(fl!("remove")) .on_press(Message::DialogComplete) .id(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("keep")).on_press(Message::DialogCancel), ), }; Some(dialog.into()) } fn footer(&self) -> Option> { if self.progress_operations.is_empty() { return None; } let cosmic_theme::Spacing { space_xs, space_s, .. } = theme::spacing(); let mut title = String::new(); let mut total_progress = 0.0; let mut count = 0; let mut all_paused = true; for (op, controller) in self.pending_operations.values() { if !controller.is_paused() { all_paused = false; } if op.show_progress_notification() { let progress = controller.progress(); if title.is_empty() { title = op.pending_text(progress, controller.state()); } total_progress += progress; count += 1; } } let running = count; // Adjust the progress bar so it does not jump around when operations finish for id in &self.progress_operations { if self.complete_operations.contains_key(id) { total_progress += 1.0; count += 1; } } let finished = count - running; total_progress /= count as f32; if running >= 1 && (running > 1 || finished > 0) { if finished > 0 { title = fl!( "operations-running-finished", running = running, finished = finished, percent = ((total_progress * 100.0) as i32) ); } else { title = fl!( "operations-running", running = running, percent = ((total_progress * 100.0) as i32) ); } } //TODO: get height from theme? let progress_bar_height = Length::Fixed(4.0); let progress_bar = widget::determinate_linear(total_progress) .width(Length::Fill) .girth(progress_bar_height); let container = widget::layer_container(widget::column::with_children([ widget::row::with_children([ progress_bar.into(), if all_paused { widget::tooltip( widget::button::icon(icon::from_name("media-playback-start-symbolic")) .on_press(Message::PendingPauseAll(false)) .padding(8), widget::text::body(fl!("resume")), widget::tooltip::Position::Top, ) .into() } else { widget::tooltip( widget::button::icon(icon::from_name("media-playback-pause-symbolic")) .on_press(Message::PendingPauseAll(true)) .padding(8), widget::text::body(fl!("pause")), widget::tooltip::Position::Top, ) .into() }, widget::tooltip( widget::button::icon(icon::from_name("window-close-symbolic")) .on_press(Message::PendingCancelAll) .padding(8), widget::text::body(fl!("cancel")), widget::tooltip::Position::Top, ) .into(), ]) .align_y(Alignment::Center) .into(), widget::text::body(title).into(), widget::space::vertical().height(space_s).into(), widget::row::with_children([ widget::button::link(fl!("details")) .on_press(Message::ToggleContextPage(ContextPage::EditHistory)) .padding(0) .trailing_icon(true) .into(), widget::space::horizontal().into(), widget::button::standard(fl!("dismiss")) .on_press(Message::PendingDismiss) .into(), ]) .align_y(Alignment::Center) .into(), ])) .padding([8, space_xs]) .layer(cosmic_theme::Layer::Primary); Some(container.into()) } fn header_start(&self) -> Vec> { vec![menu::menu_bar( &self.core, self.tab_model.active_data::(), &self.config, &self.modifiers, &self.key_binds, self.clipboard_has_content(), )] } fn header_end(&self) -> Vec> { let mut elements = Vec::with_capacity(2); if let Some(term) = self.search_get() { if self.core.is_condensed() { elements.push( //TODO: selected state is not appearing different widget::button::icon(icon::from_name("system-search-symbolic")) .on_press(Message::SearchClear) .padding(8) .selected(true) .into(), ); } else { elements.push( widget::text_input::search_input("", term) .width(Length::Fixed(240.0)) .id(self.search_id.clone()) .on_clear(Message::SearchClear) .on_input(Message::SearchInput) .into(), ); } } else { elements.push( widget::button::icon(icon::from_name("system-search-symbolic")) .on_press(Message::SearchActivate) .padding(8) .into(), ); } elements } /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let cosmic_theme::Spacing { space_xxs, space_s, .. } = theme::spacing(); let mut tab_column = widget::column::with_capacity(4); if self.core.is_condensed() && let Some(term) = self.search_get() { tab_column = tab_column.push( widget::container( widget::text_input::search_input("", term) .width(Length::Fill) .id(self.search_id.clone()) .on_clear(Message::SearchClear) .on_input(Message::SearchInput), ) .padding(space_xxs), ); } if self.tab_model.len() > 1 { tab_column = tab_column.push( widget::container( widget::tab_bar::horizontal(&self.tab_model) .button_height(32) .button_spacing(space_xxs) .enable_tab_drag(String::from("x-cosmic-files/tab-dnd")) .on_reorder(Message::ReorderTab) .tab_drag_threshold(25.) .on_activate(Message::TabActivate) .on_close(|entity| Message::TabClose(Some(entity))) .on_dnd_enter(Message::DndEnterTab) .on_dnd_leave(|_| Message::DndExitTab) .on_dnd_drop(|entity, data, action| { Message::DndDropTab(entity, data, action) }) .drag_id(self.tab_drag_id), ) .class(style::Container::Background) .width(Length::Fill) .padding([0, space_s]), ); } let entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data::(entity) { let tab_view = tab .view( &self.key_binds, &self.modifiers, self.clipboard_has_content(), &self.config.context_actions, ) .map(move |message| Message::TabMessage(Some(entity), message)); tab_column = tab_column.push(tab_view); } else { //TODO } // The toaster is added on top of an empty element to ensure that it does not override context menus tab_column = tab_column.push(widget::toaster(&self.toasts, widget::space::horizontal())); let content: Element<_> = tab_column.into(); // Uncomment to debug layout: //content.explain(cosmic::iced::Color::WHITE) content } fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> { let content = match self.windows.get(&id) { Some(window) => match &window.kind { WindowKind::ContextMenu(entity, id) => match self.tab_model.data::(*entity) { Some(tab) => { return widget::autosize::autosize( menu::context_menu( tab, &self.key_binds, &window.modifiers, self.clipboard_has_content(), &self.config.context_actions, ) .map(|x| Message::TabMessage(Some(*entity), x)), id.clone(), ) .into(); } None => widget::text("Unknown tab ID").into(), }, WindowKind::Desktop(entity) => { let mut tab_column = widget::column::with_capacity(3); let tab_view = match self.tab_model.data::(*entity) { Some(tab) => tab .view( &self.key_binds, &window.modifiers, self.clipboard_has_content(), &self.config.context_actions, ) .map(move |message| Message::TabMessage(Some(*entity), message)), None => widget::space::vertical().into(), }; tab_column = tab_column.push(tab_view); // The toaster is added on top of an empty element to ensure that it does not override context menus tab_column = tab_column.push(widget::toaster(&self.toasts, widget::space::horizontal())); return if let Some(margin) = self.margin.get(&id) { if margin.0 >= 0. || margin.2 >= 0. { tab_column = widget::column::with_children([ space::vertical().height(margin.0).into(), tab_column.into(), space::vertical().height(margin.2).into(), ]); } if margin.1 >= 0. || margin.3 >= 0. { Element::from(widget::row::with_children([ space::horizontal().width(margin.1).into(), tab_column.into(), space::horizontal().width(margin.3).into(), ])) } else { tab_column.into() } } else { tab_column.into() }; } WindowKind::DesktopViewOptions => self.desktop_view_options(), WindowKind::Dialogs(id) => match self.dialog() { Some(element) => return widget::autosize::autosize(element, id.clone()).into(), None => widget::space::horizontal().into(), }, WindowKind::Preview(entity_opt, kind) => self .preview(entity_opt, kind, false) .map(|x| Message::TabMessage(*entity_opt, x)), WindowKind::FileDialog(..) => match &self.file_dialog_opt { Some(dialog) => return dialog.view(id), None => widget::text("Unknown window ID").into(), }, }, None => { //TODO: distinct views per monitor in desktop mode return self.view_main().map(|message| match message { cosmic::Action::App(app) => app, cosmic::Action::Cosmic(cosmic) => Message::Cosmic(cosmic), cosmic::Action::None => Message::None, }); } }; widget::container(widget::id_container( widget::scrollable(content), widget::Id::new("main container for files"), )) .width(Length::Fill) .height(Length::Fill) .class(theme::Container::WindowBackground) .into() } fn system_theme_update( &mut self, _keys: &[&'static str], _new_theme: &cosmic::cosmic_theme::Theme, ) -> Task { self.update(Message::SystemThemeModeChange) } fn subscription(&self) -> Subscription { struct WatcherSubscription; struct TrashWatcherSubscription; struct TimeSubscription; #[cfg(all( not(feature = "desktop-applet"), not(target_os = "ios"), not(target_os = "android") ))] struct RecentsWatcherSubscription; let mut subscriptions = vec![ //TODO: filter more events by window id event::listen_with(|event, status, window_id| match event { Event::Mouse(mouse::Event::ButtonPressed(button)) => match status { event::Status::Ignored => Some(Message::Mouse(window_id, button)), event::Status::Captured => None, }, Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, text, .. }) => match status { event::Status::Ignored => Some(Message::Key(window_id, modifiers, key, text)), event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::ModifiersChanged(window_id, modifiers)) } #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Event::Window(WindowEvent::Focused) => Some(Message::Focused(window_id)), #[cfg(not(all(feature = "wayland", feature = "desktop-applet")))] Event::Window(WindowEvent::Focused) => Some(Message::CheckClipboard), Event::Window(WindowEvent::CloseRequested) => Some(Message::WindowClose), Event::Window(WindowEvent::Opened { position: _, size }) => { Some(Message::Size(window_id, size)) } Event::Window(WindowEvent::Resized(s)) => Some(Message::Size(window_id, s)), #[cfg(all(feature = "wayland", feature = "desktop-applet"))] Event::PlatformSpecific(event::PlatformSpecific::Wayland(wayland_event)) => { match wayland_event { WaylandEvent::Output(output_event, output) => { Some(Message::OutputEvent(output_event, output)) } #[cfg(feature = "desktop")] WaylandEvent::OverlapNotify(event, ..) => { Some(Message::Overlap(window_id, event)) } _ => None, } } _ => None, }), Config::subscription().map(|update| { if !update.errors.is_empty() { log::info!( "errors loading config {:?}: {:?}", update.keys, update.errors ); } Message::Config(update.config) }), cosmic_config::config_subscription::<_, TimeConfig>( TypeId::of::(), TIME_CONFIG_ID.into(), 1, ) .map(|update| { if !update.errors.is_empty() { log::info!( "errors loading time config {:?}: {:?}", update.keys, update.errors ); } Message::TimeConfigChange(update.config) }), Subscription::run_with(TypeId::of::(), |_| { stream::channel( 100, |mut output: futures::channel::mpsc::Sender| async move { let watcher_res = { let mut output = output.clone(); new_debouncer( time::Duration::from_millis(250), Some(time::Duration::from_millis(250)), move |events_res: notify_debouncer_full::DebounceEventResult| { match events_res { Ok(mut events) => { log::debug!("{events:?}"); events.retain(|event| { match &event.kind { notify::EventKind::Access(_) => { // Data not mutated false } notify::EventKind::Modify( notify::event::ModifyKind::Metadata(e), ) if (*e != notify::event::MetadataKind::Any && *e != notify::event::MetadataKind::WriteTime) => { // Data not mutated nor modify time changed false } _ => true } }); if !events.is_empty() { match futures::executor::block_on(async { output.send(Message::NotifyEvents(events)).await }) { Ok(()) => {} Err(err) => { log::warn!( "failed to send notify events: {err:?}" ); } } } } Err(err) => { log::warn!("failed to watch files: {err:?}"); } } }, ) }; match watcher_res { Ok(watcher) => { match output .send(Message::NotifyWatcher(WatcherWrapper { watcher_opt: Some(watcher), })) .await { Ok(()) => {} Err(err) => { log::warn!("failed to send notify watcher: {err:?}"); } } } Err(err) => { log::warn!("failed to create file watcher: {err:?}"); } } std::future::pending().await }, ) }), Subscription::run_with(TypeId::of::(), |_| { stream::channel( 1, |mut output: futures::channel::mpsc::Sender| async move { let watcher_res = new_debouncer( time::Duration::from_millis(250), Some(time::Duration::from_millis(250)), move |event_res: notify_debouncer_full::DebounceEventResult| { match event_res { Ok(events) => { // Rescan on any event. We don't need to evaluate each event // because as long as the trash changed in any way we need to // rescan. let should_rescan = events.iter().any(|event| !event.kind.is_access()); if should_rescan && let Err(e) = futures::executor::block_on(async { output.send(Message::RescanTrash).await }) { log::warn!( "trash needs to be rescanned but sending message failed: {e:?}" ); } } Err(e) => { log::warn!("failed to watch trash bin for changes: {e:?}"); } } }, ); match (watcher_res, Trash::folders()) { (Ok(mut watcher), Ok(trash_bins)) => { // Watch the "bins" themselves as well as the files folder where // trashed items are placed. This allows us to avoid recursively // watching the trash which is slow but also properly get events. let trash_paths = trash_bins .into_iter() .flat_map(|path| [path.join("files"), path]); for path in trash_paths { if let Err(e) = watcher.watch(&path, notify::RecursiveMode::NonRecursive) { log::warn!( "failed to add trash bin `{}` to watcher: {e:?}", path.display() ); } } // Don't drop the watcher std::future::pending().await } (Err(e), _) => { log::warn!("failed to create new watcher for trash bin: {e:?}"); } (_, Err(e)) => { log::warn!("could not find any valid trash bins to watch: {e:?}"); } } std::future::pending().await }, ) }), #[cfg(all( not(feature = "desktop-applet"), not(target_os = "ios"), not(target_os = "android") ))] Subscription::run_with(TypeId::of::(), |_| { stream::channel( 1, |mut output: futures::channel::mpsc::Sender| async move { let Some(recents_path) = recently_used_xbel::dir() else { log::warn!( "failed to watch recents changes: .recently_used.xbel does not exist" ); return std::future::pending().await; }; let watcher_res = new_debouncer( time::Duration::from_millis(250), Some(time::Duration::from_millis(250)), move |event_res: notify_debouncer_full::DebounceEventResult| { match event_res { Ok(events) => { // Programs differ in how they modify the recents file so the // rescan is triggered on any event but access. if events.iter().any(|event| { let kind = event.kind; kind.is_create() || kind.is_modify() || kind.is_remove() || kind.is_other() }) && let Err(e) = futures::executor::block_on(async { output.send(Message::RescanRecents).await }) { log::warn!( "open recents tabs need to be updated but sending message failed: {e:?}" ); } } Err(e) => { log::warn!( "failed to watch recents file for changes: {e:?}" ) } } }, ); match watcher_res { Ok(mut watcher) => { if let Err(e) = watcher .watch(&recents_path, notify::RecursiveMode::NonRecursive) { log::warn!( "failed to add recents file `{}` to watcher: {}", recents_path.display(), e ); } // Don't drop the watcher. std::future::pending::<()>().await; } Err(e) => { log::warn!("failed to create new watcher for recents file: {e:?}") } } std::future::pending().await }, ) }), ]; if let Some(scroll_speed) = self.auto_scroll_speed { subscriptions.push( iced::time::every(time::Duration::from_millis(10)) .with(scroll_speed) .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)), ); } subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| { mounter .subscription() .with(*key) .map(|(key, mounter_message)| match mounter_message { MounterMessage::Items(items) => Message::MounterItems(key, items), MounterMessage::MountResult(item, res) => Message::MountResult(key, item, res), MounterMessage::NetworkAuth(uri, auth, auth_tx) => { Message::NetworkAuth(key, uri, auth, auth_tx) } MounterMessage::NetworkResult(uri, res) => { Message::NetworkResult(key, uri, res) } }) })); if !self.pending_operations.is_empty() { //TODO: inhibit suspend/shutdown? if self.core.main_window_id().is_some() { // Force refresh the UI every 100ms while an operation is active. if self .pending_operations .values() .any(|(_, controller)| !controller.is_paused()) { subscriptions.push( cosmic::iced::time::every(Duration::from_millis(100)) .map(|_| Message::None), ); } } else { // Handle notification when window is closed and operations are in progress #[cfg(feature = "notify")] { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct NotificationSubscription; subscriptions.push(Subscription::run_with( TypeId::of::(), |_| { stream::channel( 1, move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move { tokio::task::spawn_blocking(move || { match notify_rust::Notification::new() .summary(&fl!("notification-in-progress")) .timeout(notify_rust::Timeout::Never) .show() { Ok(notification) => { let _ = futures::executor::block_on(async { msg_tx .send(Message::Notification(Arc::new( Mutex::new(notification), ))) .await }); } Err(err) => { log::warn!("failed to create notification: {err}"); } } }) .await .unwrap(); std::future::pending().await }, ) }, )); } } } let mut selected_previews = Vec::new(); match self.mode { Mode::App => { if self.core.window.show_context && let ContextPage::Preview(entity_opt, PreviewKind::Selected) = self.context_page { selected_previews .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active()))); } } Mode::Desktop => { for window_kind in self.windows.values().map(|window| &window.kind) { if let WindowKind::Preview(entity_opt, _) = window_kind { selected_previews .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active()))); } } } } subscriptions.extend(self.tab_model.iter().filter_map(|entity| { let tab = self.tab_model.data::(entity)?; Some( tab.subscription( selected_previews .iter() .any(|preview| preview.as_ref() == Some(entity).as_ref()), ) .with(entity) .map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)), ) })); Subscription::batch(subscriptions) } } // Utilities to build a temporary file hierarchy for tests. // // Ideally, tests would use the cap-std crate which limits path traversal. #[cfg(test)] pub(crate) mod test_utils { use std::cmp::Ordering; use std::fs::File; use std::io::{self, Write}; use std::iter; use std::path::Path; use log::{debug, trace}; use tempfile::{TempDir, tempdir}; use crate::config::{IconSizes, TabConfig, ThumbCfg}; use crate::tab::Item; use super::*; // Default number of files, directories, and nested directories for test file system pub const NUM_FILES: usize = 2; pub const NUM_HIDDEN: usize = 1; pub const NUM_DIRS: usize = 2; pub const NUM_NESTED: usize = 1; pub const NAME_LEN: usize = 5; /// Add `n` temporary files in `dir` /// /// Each file is assigned a numeric name from [0, n) with a prefix. pub fn file_flat_hier>(dir: D, n: usize, prefix: &str) -> io::Result> { let dir = dir.as_ref(); (0..n) .map(|i| -> io::Result { let name = format!("{prefix}{i}"); let path = dir.join(&name); let mut file = File::create(path)?; file.write_all(name.as_bytes())?; Ok(file) }) .collect() } // Random alphanumeric String of length `len` fn rand_string(len: usize) -> String { let mut rng = fastrand::Rng::new(); iter::repeat_with(|| rng.alphanumeric()).take(len).collect() } /// Create a small, temporary file hierarchy. /// /// # Arguments /// /// * `files` - Number of files to create in temp directories /// * `hidden` - Number of hidden files to create /// * `dirs` - Number of directories to create /// * `nested` - Number of nested directories to create in new dirs /// * `name_len` - Length of randomized directory names pub fn simple_fs( files: usize, hidden: usize, dirs: usize, nested: usize, name_len: usize, ) -> io::Result { // Files created inside of a TempDir are deleted with the directory // TempDir won't leak resources as long as the destructor runs let root = tempdir()?; debug!("Root temp directory: {}", root.as_ref().display()); trace!( "Creating {files} files and {hidden} hidden files in {dirs} temp dirs with {nested} nested temp dirs" ); // All paths for directories and nested directories let paths = iter::repeat_with(|| { let root = root.as_ref(); let current = rand_string(name_len); iter::once(root.join(¤t)).chain( iter::repeat_with(move || { let mut path = root.join(¤t); path.push(rand_string(name_len)); path }) .take(nested), ) }) .take(dirs) .flatten(); // Create directories from `paths` and add a few files for path in paths { fs::create_dir_all(&path)?; // Normal files file_flat_hier(&path, files, "")?; // Hidden files file_flat_hier(&path, hidden, ".")?; for entry in path.read_dir()? { let entry = entry?; if entry.file_type()?.is_file() { trace!("Created file: {}", entry.path().display()); } } } Ok(root) } /// Empty file hierarchy pub fn empty_fs() -> io::Result { tempdir() } /// Sort files. /// /// Directories are placed before files. /// Files are lexically sorted. /// This is more or less copied right from the [Tab] code pub fn sort_files(a: &Path, b: &Path) -> Ordering { match (a.is_dir(), b.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => LANGUAGE_SORTER.compare( a.file_name() .expect("temp entries should have names") .to_str() .expect("temp entries should be valid UTF-8"), b.file_name() .expect("temp entries should have names") .to_str() .expect("temp entries should be valid UTF-8"), ), } } /// Read directory entries from `path` and sort. pub fn read_dir_sorted(path: &Path) -> io::Result> { let mut entries: Vec<_> = path .read_dir()? .map(|maybe_entry| maybe_entry.map(|entry| entry.path())) .collect::>()?; entries.sort_by(|a, b| sort_files(a, b)); Ok(entries) } /// Filter `path` for directories pub fn filter_dirs(path: &Path) -> io::Result + use<>> { Ok(path.read_dir()?.filter_map(|entry| { entry.ok().and_then(|entry| { let path = entry.path(); path.is_dir().then_some(path) }) })) } // Filter `path` for files pub fn filter_files(path: &Path) -> io::Result + use<>> { Ok(path.read_dir()?.filter_map(|entry| { entry.ok().and_then(|entry| { let path = entry.path(); path.is_file().then_some(path) }) })) } /// Boiler plate for Tab tests pub fn tab_click_new( files: usize, hidden: usize, dirs: usize, nested: usize, name_len: usize, ) -> io::Result<(TempDir, Tab)> { let fs = simple_fs(files, hidden, dirs, nested, name_len)?; let path = fs.path(); // New tab with items let location = Location::Path(path.to_owned()); let (parent_item_opt, items) = location.scan(IconSizes::default()); let mut tab = Tab::new( location, TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); tab.parent_item_opt = parent_item_opt; tab.set_items(items); // Ensure correct number of directories as a sanity check let items = tab.items_opt().expect("tab should be populated with Items"); assert_eq!(NUM_DIRS, items.len()); Ok((fs, tab)) } /// Equality for [Path] and [Item]. pub fn eq_path_item(path: &Path, item: &Item) -> bool { let name = path .file_name() .expect("temp entries should have names") .to_str() .expect("temp entries should be valid UTF-8"); let is_dir = path.is_dir(); // NOTE: I don't want to change `tab::hidden_attribute` to `pub(crate)` for // tests without asking #[cfg(not(target_os = "windows"))] let is_hidden = name.starts_with('.'); #[cfg(target_os = "windows")] let is_hidden = { use std::os::windows::fs::MetadataExt; const FILE_ATTRIBUTE_HIDDEN: u32 = 2; let metadata = path.metadata().expect("fetching file metadata"); metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN }; name == item.name && is_dir == item.metadata.is_dir() && path == item.path_opt().expect("item should have path") && is_hidden == item.hidden } /// Asserts `tab`'s location changed to `path` pub fn assert_eq_tab_path(tab: &Tab, path: &Path) { // Paths should be the same let Some(tab_path) = tab.location.path_opt() else { panic!("Expected tab's location to be a path"); }; assert_eq!( path, tab_path, "Tab's path is {} instead of being updated to {}", tab_path.display(), path.display() ); } /// Assert that tab's items are equal to a path's entries. pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) { let Some(tab_path) = tab.location.path_opt() else { panic!("Expected tab's location to be a path"); }; // Tab items are sorted so paths from read_dir must be too let entries = read_dir_sorted(path).expect("should be able to read paths from temp dir"); // Check lengths. // `items_opt` is optional and the directory at `path` may have zero entries // Therefore, this doesn't panic if `items_opt` is None let items_len = tab.items_opt().map(Vec::len).unwrap_or_default(); assert_eq!(entries.len(), items_len); assert!( entries .into_iter() .zip(tab.items_opt().map_or([].as_slice(), Vec::as_slice)) .all(|(a, b)| eq_path_item(&a, b)), "Path ({}) and Tab path ({}) don't have equal contents", path.display(), tab_path.display() ); } } ================================================ FILE: src/archive.rs ================================================ use crate::mime_icon::mime_for_path; use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}; use cosmic::iced::futures; 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] = &[ "application/gzip", "application/x-compressed-tar", "application/x-tar", "application/zip", #[cfg(feature = "bzip2")] "application/x-bzip", #[cfg(feature = "bzip2")] "application/x-bzip-compressed-tar", #[cfg(feature = "bzip2")] "application/x-bzip2", #[cfg(feature = "bzip2")] "application/x-bzip2-compressed-tar", #[cfg(feature = "lzma-rust2")] "application/x-xz", #[cfg(feature = "lzma-rust2")] "application/x-xz-compressed-tar", ]; pub const SUPPORTED_EXTENSIONS: &[&str] = &[ ".tar.bz2", ".tar.gz", ".tar.lzma", ".tar.xz", ".tgz", ".tar", ".zip", ]; pub fn extract( path: &Path, new_dir: &Path, password: &Option, controller: &Controller, ) -> Result<(), OperationError> { let mime = mime_for_path(path, None, false); let password = password.as_deref(); match mime.essence_str() { "application/gzip" | "application/x-compressed-tar" => { OpReader::new(path, controller.clone()) .map(io::BufReader::new) .map(flate2::read::GzDecoder::new) .map(tar::Archive::new) .and_then(|mut archive| archive.unpack(new_dir)) .map_err(|e| OperationError::from_err(e, controller))?; } "application/x-tar" => OpReader::new(path, controller.clone()) .map(io::BufReader::new) .map(tar::Archive::new) .and_then(|mut archive| archive.unpack(new_dir)) .map_err(|e| OperationError::from_err(e, controller))?, "application/zip" => fs::File::open(path) .map(io::BufReader::new) .map(zip::ZipArchive::new) .map_err(|e| OperationError::from_err(e, controller))? .and_then(move |mut archive| { zip_extract(&mut archive, new_dir, password, controller.clone()) }) .map_err(|e| match e { ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED) | ZipError::InvalidPassword => { OperationError::from_kind(OperationErrorType::PasswordRequired, controller) } _ => OperationError::from_err(e, controller), })?, #[cfg(feature = "bzip2")] "application/x-bzip" | "application/x-bzip-compressed-tar" | "application/x-bzip2" | "application/x-bzip2-compressed-tar" => OpReader::new(path, controller.clone()) .map(io::BufReader::new) .map(bzip2::read::BzDecoder::new) .map(tar::Archive::new) .and_then(|mut archive| archive.unpack(new_dir)) .map_err(|e| OperationError::from_err(e, controller))?, #[cfg(feature = "lzma-rust2")] "application/x-xz" | "application/x-xz-compressed-tar" => { OpReader::new(path, controller.clone()) .map(io::BufReader::new) .map(|reader| lzma_rust2::XzReader::new(reader, true)) .map(tar::Archive::new) .and_then(|mut archive| archive.unpack(new_dir)) .map_err(|e| OperationError::from_err(e, controller))?; } _ => Err(OperationError::from_err( format!("unsupported mime type {mime:?}"), controller, ))?, } Ok(()) } // From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added fn zip_extract>( archive: &mut zip::ZipArchive, directory: P, password: Option<&str>, controller: Controller, ) -> zip::result::ZipResult<()> { use std::ffi::OsString; use std::fs; use zip::result::ZipError; fn make_writable_dir_all>( outpath: T, target_dirs: &mut HashSet, ) -> Result<(), ZipError> { let path = outpath.as_ref(); if !path.exists() { fs::create_dir_all(path)?; } if !target_dirs.contains(path) { target_dirs.insert(path.to_path_buf()); } #[cfg(unix)] { // Dirs must be writable until all normal files are extracted use std::os::unix::fs::PermissionsExt; fs::set_permissions( path, fs::Permissions::from_mode(0o700 | fs::metadata(path)?.permissions().mode()), )?; } Ok(()) } let mut buffer = vec![0; 4 * 1024 * 1024]; let total_files = archive.len(); let mut written_files = Vec::with_capacity(total_files); let mut target_dirs = HashSet::new(); #[cfg(unix)] let mut files_by_unix_mode = Vec::with_capacity(total_files); let mut files_by_last_modified = Vec::with_capacity(total_files); for i in 0..total_files { futures::executor::block_on(async { controller .check() .await .map_err(|s| io::Error::other(OperationError::from_state(s, &controller))) })?; controller.set_progress(i as f32 / total_files as f32); let mut file = match password { None => archive.by_index(i), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), }?; let filepath = file .enclosed_name() .ok_or(ZipError::InvalidArchive("Invalid file path".into()))?; let outpath = directory.as_ref().join(filepath); if let Some(last_modified) = file.last_modified() { files_by_last_modified.push((outpath.clone(), last_modified)); } if file.is_dir() { make_writable_dir_all(&outpath, &mut target_dirs)?; #[cfg(unix)] if let Some(mode) = file.unix_mode() { files_by_unix_mode.push((outpath, mode)); } continue; } if let Some(parent) = outpath.parent() { make_writable_dir_all(parent, &mut target_dirs)?; } 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)] { use std::os::unix::ffi::OsStringExt; let target = OsString::from_vec(target); std::os::unix::fs::symlink(&target, outpath.as_path())?; } #[cfg(windows)] { let Ok(target) = String::from_utf8(target) else { return Err(ZipError::InvalidArchive( "Invalid UTF-8 as symlink target".into(), )); }; 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 } else if let Ok(meta) = std::fs::metadata(&target_path) { meta.is_dir() } else { false }; if target_is_dir { std::os::windows::fs::symlink_dir(target_path, outpath.as_path())?; } else { std::os::windows::fs::symlink_file(target_path, outpath.as_path())?; } } written_files.push(outpath); continue; } let total = file.size(); let mut outfile = fs::File::create(&outpath)?; let mut current = 0; loop { futures::executor::block_on(async { controller .check() .await .map_err(|s| io::Error::other(OperationError::from_state(s, &controller))) })?; let count = file.read(&mut buffer)?; if count == 0 { break; } outfile.write_all(&buffer[..count])?; current += count as u64; if current < total { let file_progress = current as f32 / total as f32; let total_progress = (i as f32 + file_progress) / total_files as f32; controller.set_progress(total_progress); } } // Check for real permissions, which we'll set in a second pass #[cfg(unix)] if let Some(mode) = file.unix_mode() { files_by_unix_mode.push((outpath.clone(), mode)); } written_files.push(outpath); } #[cfg(unix)] { use std::cmp::Reverse; use std::os::unix::fs::PermissionsExt; if files_by_unix_mode.len() > 1 { // Ensure we update children's permissions before making a parent unwritable files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.components().count())); } for (path, mode) in files_by_unix_mode { fs::set_permissions(&path, fs::Permissions::from_mode(mode))?; } } for (path, last_modified) in files_by_last_modified { if let Some(modified) = zip_date_time_to_system_time(last_modified) { let file_time = filetime::FileTime::from_system_time(modified); filetime::set_file_mtime(&path, file_time)?; } } // Flush files to disk futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await }); Ok(()) } fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option { 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 { let date_time = Zoned::try_from(system_time).ok()?; zip::DateTime::from_date_and_time( date_time.year() as u16, date_time.month() as u8, date_time.day() as u8, date_time.hour() as u8, date_time.minute() as u8, date_time.second() as u8, ) .ok() } ================================================ FILE: src/channel.rs ================================================ // Copyright 2025 System76 // 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() -> (Sender, Receiver) { 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 { pub(self) queue: Mutex>, /// 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(Arc>); impl Sender { pub fn send(&self, message: Message) { self.0.queue.lock().unwrap().push_back(message); self.0.notify.notify_one(); } } impl Drop for Sender { fn drop(&mut self) { self.0.closed.store(true, Ordering::SeqCst); self.0.notify.notify_one(); } } pub struct Receiver(Arc>); impl Receiver { /// Returns a value until the sender is dropped. pub async fn recv(&self) -> Option { 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 { self.0.queue.lock().unwrap().pop_front() } } ================================================ FILE: src/clipboard.rs ================================================ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; use std::borrow::Cow; use std::error::Error; use std::path::{Path, PathBuf}; use std::str; use url::Url; #[derive(Clone, Copy, Debug)] pub enum ClipboardKind { Copy, Cut { is_dnd: bool }, } #[derive(Clone, Debug)] pub struct ClipboardCopy { pub available: Cow<'static, [String]>, pub text_plain: Cow<'static, [u8]>, pub text_uri_list: Cow<'static, [u8]>, pub x_special_gnome_copied_files: Cow<'static, [u8]>, } impl ClipboardCopy { pub fn new>(kind: ClipboardKind, paths: impl IntoIterator) -> Self { let available = vec![ "text/plain".to_string(), "text/plain;charset=utf-8".to_string(), "UTF8_STRING".to_string(), "text/uri-list".to_string(), "x-special/gnome-copied-files".to_string(), ]; let mut text_plain = String::new(); let mut text_uri_list = String::new(); let mut x_special_gnome_copied_files = match kind { ClipboardKind::Copy => "copy", ClipboardKind::Cut { .. } => "cut", } .to_string(); //TODO: do we have to use \r\n? let cr_nl = "\r\n"; for path in paths { let path = path.as_ref(); match path.to_str() { Some(path_str) => { if !text_plain.is_empty() { text_plain.push_str(cr_nl); } //TODO: what if the path contains CR or NL? text_plain.push_str(path_str); } None => { //TODO: allow non-UTF-8? log::warn!( "{} is not valid UTF-8, not adding to text/plain clipboard", path.display() ); } } match Url::from_file_path(path) { Ok(url) => { let url_str = url.as_ref(); text_uri_list.push_str(url_str); text_uri_list.push_str(cr_nl); x_special_gnome_copied_files.push('\n'); x_special_gnome_copied_files.push_str(url_str); } Err(()) => { log::warn!( "{} cannot be turned into a URL, not adding to text/uri-list clipboard", path.display() ); } } } Self { available: Cow::from(available), text_plain: Cow::from(text_plain.into_bytes()), text_uri_list: Cow::from(text_uri_list.into_bytes()), x_special_gnome_copied_files: Cow::from(x_special_gnome_copied_files.into_bytes()), } } } impl AsMimeTypes for ClipboardCopy { fn available(&self) -> Cow<'static, [String]> { self.available.clone() } fn as_bytes(&self, mime_type: &str) -> Option> { match mime_type { "text/plain" | "text/plain;charset=utf-8" | "UTF8_STRING" => { Some(self.text_plain.clone()) } "text/uri-list" => Some(self.text_uri_list.clone()), "x-special/gnome-copied-files" => Some(self.x_special_gnome_copied_files.clone()), _ => None, } } } #[derive(Clone, Debug)] pub struct ClipboardPaste { pub kind: ClipboardKind, pub paths: Vec, } impl AllowedMimeTypes for ClipboardPaste { fn allowed() -> Cow<'static, [String]> { Cow::from(vec![ "x-special/gnome-copied-files".to_string(), "text/uri-list".to_string(), ]) } } impl TryFrom<(Vec, String)> for ClipboardPaste { type Error = Box; fn try_from(value: (Vec, String)) -> Result { let (data, mime) = value; // 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)?; let _lines = text.lines(); for line in text.lines() { let url = Url::parse(line)?; match url.to_file_path() { Ok(path) => paths.push(path), Err(()) => Err(format!("invalid file URL {url:?}"))?, } } } "x-special/gnome-copied-files" => { let text = str::from_utf8(&data)?; for (i, line) in text.lines().enumerate() { if i == 0 { kind = match line { "copy" => ClipboardKind::Copy, "cut" => ClipboardKind::Cut { is_dnd: false }, _ => Err(format!("unsupported clipboard operation {line:?}"))?, }; } else { let url = Url::parse(line)?; match url.to_file_path() { Ok(path) => paths.push(path), Err(()) => Err(format!("invalid file URL {url:?}"))?, } } } } _ => Err(format!("unsupported mime type {mime:?}"))?, } Ok(Self { kind, paths }) } } /// Image data from clipboard for pasting as a new file. #[derive(Clone, Debug)] pub struct ClipboardPasteImage { pub data: Vec, pub mime_type: String, } impl AllowedMimeTypes for ClipboardPasteImage { fn allowed() -> Cow<'static, [String]> { Cow::from(vec![ "image/png".to_string(), "image/jpeg".to_string(), "image/gif".to_string(), "image/bmp".to_string(), "image/webp".to_string(), "image/tiff".to_string(), "image/x-tiff".to_string(), "image/svg+xml".to_string(), "image/x-icon".to_string(), "image/vnd.microsoft.icon".to_string(), "image/x-bmp".to_string(), "image/x-ms-bmp".to_string(), "image/pjpeg".to_string(), "image/x-png".to_string(), "image/avif".to_string(), "image/heic".to_string(), "image/heif".to_string(), "image/jxl".to_string(), ]) } } impl TryFrom<(Vec, String)> for ClipboardPasteImage { type Error = Box; fn try_from(value: (Vec, String)) -> Result { let (data, mime) = value; if data.is_empty() { return Err("Empty image data".into()); } Ok(Self { data, mime_type: mime, }) } } impl ClipboardPasteImage { /// Get the file extension for the image based on MIME type. /// Returns None if the MIME type is not recognized. pub fn extension(&self) -> Option<&'static str> { match self.mime_type.as_str() { "image/png" | "image/x-png" => Some("png"), "image/jpeg" | "image/pjpeg" => Some("jpg"), "image/gif" => Some("gif"), "image/bmp" | "image/x-bmp" | "image/x-ms-bmp" => Some("bmp"), "image/webp" => Some("webp"), "image/tiff" | "image/x-tiff" => Some("tiff"), "image/svg+xml" => Some("svg"), "image/x-icon" | "image/vnd.microsoft.icon" => Some("ico"), "image/avif" => Some("avif"), "image/heic" => Some("heic"), "image/heif" => Some("heif"), "image/jxl" => Some("jxl"), _ => None, } } } /// Video data from clipboard for pasting as a new file. #[derive(Clone, Debug)] pub struct ClipboardPasteVideo { pub data: Vec, pub mime_type: String, } impl AllowedMimeTypes for ClipboardPasteVideo { fn allowed() -> Cow<'static, [String]> { Cow::from(vec![ "video/mp4".to_string(), "video/webm".to_string(), "video/ogg".to_string(), "video/mpeg".to_string(), "video/quicktime".to_string(), "video/x-msvideo".to_string(), "video/x-matroska".to_string(), "video/x-flv".to_string(), "video/3gpp".to_string(), "video/3gpp2".to_string(), "video/x-ms-wmv".to_string(), "video/avi".to_string(), ]) } } impl TryFrom<(Vec, String)> for ClipboardPasteVideo { type Error = Box; fn try_from(value: (Vec, String)) -> Result { let (data, mime) = value; if data.is_empty() { return Err("Empty video data".into()); } Ok(Self { data, mime_type: mime, }) } } impl ClipboardPasteVideo { /// Get the file extension for the video based on MIME type. /// Returns None if the MIME type is not recognized. pub fn extension(&self) -> Option<&'static str> { match self.mime_type.as_str() { "video/mp4" => Some("mp4"), "video/webm" => Some("webm"), "video/ogg" => Some("ogv"), "video/mpeg" => Some("mpeg"), "video/quicktime" => Some("mov"), "video/x-msvideo" | "video/avi" => Some("avi"), "video/x-matroska" => Some("mkv"), "video/x-flv" => Some("flv"), "video/3gpp" => Some("3gp"), "video/3gpp2" => Some("3g2"), "video/x-ms-wmv" => Some("wmv"), _ => None, } } } /// Text data from clipboard for pasting as a new text file. #[derive(Clone, Debug)] pub struct ClipboardPasteText { pub data: String, } impl AllowedMimeTypes for ClipboardPasteText { fn allowed() -> Cow<'static, [String]> { Cow::from(vec![ "text/plain".to_string(), "text/plain;charset=utf-8".to_string(), "UTF8_STRING".to_string(), "STRING".to_string(), "TEXT".to_string(), ]) } } impl TryFrom<(Vec, String)> for ClipboardPasteText { type Error = Box; fn try_from(value: (Vec, String)) -> Result { let (data, _mime) = value; if data.is_empty() { return Err("Empty text data".into()); } // Use lossy conversion to handle clipboard data that may contain // invalid UTF-8 (e.g., Latin-1 encoded special characters from browsers) let text = String::from_utf8_lossy(&data); Ok(Self { data: text.into_owned(), }) } } /// Cached clipboard content for paste operations. /// This is needed because Wayland restricts clipboard access from popup windows. #[derive(Clone, Debug)] pub enum ClipboardCache { Files(ClipboardPaste), Image(ClipboardPasteImage), Video(ClipboardPasteVideo), Text(ClipboardPasteText), Empty, } ================================================ FILE: src/config.rs ================================================ // SPDX-License-Identifier: GPL-3.0-only use std::any::TypeId; use std::num::NonZeroU16; use std::path::PathBuf; 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; use crate::app::App; use crate::tab::{HeadingOptions, Location, View}; pub use crate::context_action::{ContextActionPreset, ContextActionSelection}; pub const CONFIG_VERSION: u64 = 1; // Default icon sizes pub const ICON_SIZE_LIST: u16 = 32; pub const ICON_SIZE_LIST_CONDENSED: u16 = 48; pub const ICON_SIZE_GRID: u16 = 64; // TODO: 5 is an arbitrary number. Maybe there's a better icon size max pub const ICON_SCALE_MAX: u16 = 5; macro_rules! percent { ($perc:expr, $pixel:ident) => { (($perc.get() as f32 * $pixel as f32) / 100.).clamp(1., ($pixel * ICON_SCALE_MAX) as _) }; } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum AppTheme { Dark, Light, System, } impl AppTheme { pub fn theme(&self) -> theme::Theme { match self { Self::Dark => { let mut t = theme::system_dark(); t.theme_type.prefer_dark(Some(true)); t } Self::Light => { let mut t = theme::system_light(); t.theme_type.prefer_dark(Some(false)); t } Self::System => theme::system_preference(), } } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum Favorite { Home, Documents, Downloads, Music, Pictures, Videos, Path(PathBuf), Network { uri: String, name: String, path: PathBuf, }, } impl Favorite { pub fn from_path(path: PathBuf) -> Self { // Ensure that special folders are handled properly [ Self::Home, Self::Documents, Self::Downloads, Self::Music, Self::Pictures, Self::Videos, ] .into_iter() .find(|fav| fav.path_opt().as_ref() == Some(&path)) .unwrap_or(Self::Path(path)) } pub fn path_opt(&self) -> Option { match self { Self::Home => dirs::home_dir(), Self::Documents => dirs::document_dir(), Self::Downloads => dirs::download_dir(), Self::Music => dirs::audio_dir(), Self::Pictures => dirs::picture_dir(), Self::Videos => dirs::video_dir(), Self::Path(path) => Some(path.clone()), Self::Network { path, .. } => Some(path.clone()), } } } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum TypeToSearch { Recursive, EnterPath, SelectByPrefix, } #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(default)] pub struct State { pub sort_names: FxOrderMap, } impl Default for State { fn default() -> Self { Self { sort_names: FxOrderMap::from_iter(dirs::download_dir().into_iter().map(|dir| { ( Location::Path(dir).normalize().to_string(), (HeadingOptions::Modified, false), ) })), } } } impl State { pub fn load() -> (Option, Self) { match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) { Ok(config_handler) => { let config = match Self::get_entry(&config_handler) { Ok(ok) => ok, Err((errs, config)) => { log::info!("errors loading config: {errs:?}"); config } }; (Some(config_handler), config) } Err(err) => { log::error!("failed to create config handler: {err}"); (None, Self::default()) } } } pub fn subscription() -> Subscription> { struct ConfigSubscription; cosmic_config::config_state_subscription( TypeId::of::(), App::APP_ID.into(), CONFIG_VERSION, ) } } #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(default)] pub struct Config { pub app_theme: AppTheme, pub dialog: DialogConfig, pub desktop: DesktopConfig, pub context_actions: Vec, pub thumb_cfg: ThumbCfg, pub favorites: Vec, pub show_details: bool, pub show_recents: bool, pub tab: TabConfig, pub type_to_search: TypeToSearch, } impl Config { pub fn load() -> (Option, Self) { match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { Ok(config_handler) => { let config = match Self::get_entry(&config_handler) { Ok(ok) => ok, Err((errs, config)) => { log::info!("errors loading config: {errs:?}"); config } }; (Some(config_handler), config) } Err(err) => { log::error!("failed to create config handler: {err}"); (None, Self::default()) } } } pub fn subscription() -> Subscription> { struct ConfigSubscription; cosmic_config::config_subscription( TypeId::of::(), App::APP_ID.into(), CONFIG_VERSION, ) } /// Construct tab config for dialog pub const fn dialog_tab(&self) -> TabConfig { TabConfig { folders_first: self.dialog.folders_first, icon_sizes: self.dialog.icon_sizes, military_time: self.tab.military_time, show_hidden: self.dialog.show_hidden, single_click: false, view: self.dialog.view, } } } impl Default for Config { fn default() -> Self { Self { app_theme: AppTheme::System, desktop: DesktopConfig::default(), dialog: DialogConfig::default(), context_actions: Vec::new(), thumb_cfg: ThumbCfg::default(), favorites: vec![ Favorite::Home, Favorite::Documents, Favorite::Downloads, Favorite::Music, Favorite::Pictures, Favorite::Videos, ], show_details: false, show_recents: true, tab: TabConfig::default(), type_to_search: TypeToSearch::Recursive, } } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct DesktopConfig { pub grid_spacing: NonZeroU16, pub icon_size: NonZeroU16, pub show_content: bool, pub show_mounted_drives: bool, pub show_trash: bool, } impl Default for DesktopConfig { fn default() -> Self { Self { grid_spacing: 100.try_into().unwrap(), icon_size: 100.try_into().unwrap(), show_content: true, show_mounted_drives: false, show_trash: false, } } } impl DesktopConfig { pub fn grid_spacing_for(&self, space: u16) -> u16 { percent!(self.grid_spacing, space) as _ } } #[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct DialogConfig { /// Show folders before files pub folders_first: bool, /// Icon zoom pub icon_sizes: IconSizes, /// Show details sidebar pub show_details: bool, /// Show hidden files and folders pub show_hidden: bool, /// Selected view, grid or list pub view: View, } impl Default for DialogConfig { fn default() -> Self { Self { folders_first: false, icon_sizes: IconSizes::default(), show_details: true, show_hidden: false, view: View::List, } } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct ThumbCfg { pub jobs: NonZeroU16, pub max_mem_mb: NonZeroU16, pub max_size_mb: NonZeroU16, } impl Default for ThumbCfg { fn default() -> Self { Self { jobs: 4.try_into().unwrap(), max_mem_mb: 2000.try_into().unwrap(), max_size_mb: 64.try_into().unwrap(), } } } /// Global and local [`crate::tab::Tab`] config. /// /// [`TabConfig`] contains options that are passed to each instance of [`crate::tab::Tab`]. /// These options are set globally through the main config, but each tab may change options /// locally. Local changes aren't saved to the main config. #[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct TabConfig { /// Show folders before files pub folders_first: bool, /// Icon zoom pub icon_sizes: IconSizes, #[serde(skip)] /// 24 hour clock; this is neither serialized nor deserialized because we use the user's global /// preference rather than save it pub military_time: bool, /// Show hidden files and folders pub show_hidden: bool, /// Single click to open pub single_click: bool, /// Selected view, grid or list pub view: View, } impl Default for TabConfig { fn default() -> Self { Self { folders_first: true, icon_sizes: IconSizes::default(), military_time: false, show_hidden: false, single_click: false, view: View::List, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] #[serde(default)] pub struct IconSizes { pub list: NonZeroU16, pub grid: NonZeroU16, } impl Default for IconSizes { fn default() -> Self { Self { list: 100.try_into().unwrap(), grid: 100.try_into().unwrap(), } } } impl IconSizes { pub fn list(&self) -> u16 { percent!(self.list, ICON_SIZE_LIST) as _ } pub fn list_condensed(&self) -> u16 { percent!(self.list, ICON_SIZE_LIST_CONDENSED) as _ } pub fn grid(&self) -> u16 { percent!(self.grid, ICON_SIZE_GRID) as _ } } pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime"; #[derive(Debug, Default, Clone, CosmicConfigEntry, PartialEq, Eq)] #[version = 1] pub struct TimeConfig { pub military_time: bool, } ================================================ FILE: src/context_action.rs ================================================ // 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, } 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}`"); } } ================================================ FILE: src/dialog.rs ================================================ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only 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::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; 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, }; 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); #[derive(Clone, Debug)] pub enum DialogResult { Cancel, Open(Vec), } #[derive(Clone, Debug)] pub enum DialogKind { OpenFile, OpenFolder, OpenMultipleFiles, OpenMultipleFolders, SaveFile { filename: String }, } impl DialogKind { pub fn title(&self) -> String { match self { Self::OpenFile => fl!("open-file"), Self::OpenFolder => fl!("open-folder"), Self::OpenMultipleFiles => fl!("open-multiple-files"), Self::OpenMultipleFolders => fl!("open-multiple-folders"), Self::SaveFile { .. } => fl!("save-file"), } } pub fn accept_label(&self) -> String { match self { Self::SaveFile { .. } => fl!("save"), _ => fl!("open"), } } pub const fn is_dir(&self) -> bool { matches!(self, Self::OpenFolder | Self::OpenMultipleFolders) } pub const fn multiple(&self) -> bool { matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders) } pub const fn save(&self) -> bool { matches!(self, Self::SaveFile { .. }) } } #[derive(Clone, Debug)] pub struct DialogChoiceOption { pub id: String, pub label: String, } impl AsRef for DialogChoiceOption { fn as_ref(&self) -> &str { &self.label } } #[derive(Clone, Debug)] pub enum DialogChoice { CheckBox { id: String, label: String, value: bool, }, ComboBox { id: String, label: String, options: Vec, selected: Option, }, } #[derive(Clone, Debug)] pub enum DialogFilterPattern { Glob(String), Mime(String), } #[derive(Clone, Debug)] pub struct DialogFilter { pub label: String, pub patterns: Vec, } impl AsRef for DialogFilter { fn as_ref(&self) -> &str { &self.label } } #[derive(Clone, Debug)] pub struct DialogLabelSpan { pub text: String, pub underline: bool, } #[derive(Clone, Debug)] pub struct DialogLabel { pub spans: Vec, pub key_bind_opt: Option, } impl> From for DialogLabel { fn from(text: T) -> Self { let mut spans = Vec::::new(); let mut key_bind_opt = None; let mut next_underline = false; for c in text.as_ref().chars() { let underline = next_underline; next_underline = false; if c == '_' && !underline { next_underline = true; continue; } if underline && key_bind_opt.is_none() { key_bind_opt = Some(KeyBind { modifiers: vec![Modifier::Alt], key: Key::Character(c.to_lowercase().to_string().into()), }); } if let Some(span) = spans.last_mut() && underline == span.underline { span.text.push(c); continue; } spans.push(DialogLabelSpan { text: String::from(c), underline, }); } Self { spans, key_bind_opt, } } } impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> { fn from(label: &'a DialogLabel) -> Self { let mut iced_spans: Vec> = Vec::with_capacity(label.spans.len()); for span in &label.spans { iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline)); } cosmic::iced::widget::rich_text(iced_spans).into() } } pub struct DialogSettings { app_id: String, kind: DialogKind, path_opt: Option, } impl DialogSettings { pub fn new() -> Self { Default::default() } pub fn app_id(mut self, app_id: String) -> Self { self.app_id = app_id; self } pub fn kind(mut self, kind: DialogKind) -> Self { self.kind = kind; self } pub fn path(mut self, path: PathBuf) -> Self { self.path_opt = Some(path); self } } impl Default for DialogSettings { fn default() -> Self { Self { app_id: App::APP_ID.to_string(), kind: DialogKind::OpenFile, path_opt: None, } } } pub struct Dialog { cosmic: Cosmic, mapper: fn(DialogMessage) -> M, on_result: Box M>, } impl Dialog { pub fn new( dialog_settings: DialogSettings, mapper: fn(DialogMessage) -> M, on_result: impl Fn(DialogResult) -> M + 'static, ) -> (Self, Task) { //TODO: only do this once somehow? crate::localize::localize(); let (config_handler, config) = Config::load(); let mut settings = window::Settings { decorations: false, exit_on_close_request: false, min_size: Some(Size::new(360.0, 180.0)), resizable: true, size: Size::new(1024.0, 640.0), transparent: true, ..Default::default() }; #[cfg(target_os = "linux")] { settings.platform_specific.application_id = dialog_settings.app_id; } let (window_id, window_command) = window::open(settings); let mut core = Core::default(); core.set_main_window_id(Some(window_id)); let flags = Flags { kind: dialog_settings.kind, path_opt: dialog_settings.path_opt.as_ref().and_then(|path| { match fs::canonicalize(path) { Ok(ok) => Some(ok), Err(err) => { log::warn!("failed to canonicalize {}: {}", path.display(), err); None } } }), window_id, config_handler, config, }; let (cosmic, cosmic_command) = Cosmic::::init((core, flags)); ( Self { cosmic, mapper, on_result: Box::new(on_result), }, Task::batch([ window_command.map(|_id| cosmic::action::none()), cosmic_command .map(DialogMessage) .map(move |message| cosmic::action::app(mapper(message))), ]), ) } pub fn set_title(&mut self, title: impl Into) -> Task { let mapper = self.mapper; self.cosmic.app.title = title.into(); self.cosmic .app .update_title() .map(DialogMessage) .map(move |message| cosmic::action::app(mapper(message))) } pub fn set_accept_label(&mut self, accept_label: impl AsRef) { self.cosmic.app.accept_label = DialogLabel::from(accept_label); } pub fn choices(&self) -> &[DialogChoice] { &self.cosmic.app.choices } pub fn set_choices(&mut self, choices: impl Into>) { self.cosmic.app.choices = choices.into(); } pub fn filters(&self) -> (&[DialogFilter], Option) { (&self.cosmic.app.filters, self.cosmic.app.filter_selected) } pub fn set_filters( &mut self, filters: impl Into>, filter_selected: Option, ) -> Task { let mapper = self.mapper; self.cosmic.app.filters = filters.into(); self.cosmic.app.filter_selected = filter_selected; self.cosmic .app .rescan_tab(None) .map(DialogMessage) .map(move |message| cosmic::action::app(mapper(message))) } pub fn subscription(&self) -> Subscription { self.cosmic .subscription() .map(DialogMessage) .with(self.mapper) .map(|(mapper, message)| mapper(message)) } pub fn update(&mut self, message: DialogMessage) -> Task { let mapper = self.mapper; let command = self .cosmic .update(message.0) .map(DialogMessage) .map(move |message| cosmic::action::app(mapper(message))); if let Some(result) = self.cosmic.app.result_opt.take() { #[cfg(feature = "wayland")] if !self.cosmic.surface_views.is_empty() { log::debug!("waiting for surfaces to close..."); let mut tasks = Vec::new(); for id in self.cosmic.surface_views.iter() { match id.1.1 { SurfaceIdWrapper::Window(id) => { tasks.push(window::close::(id).discard()); } SurfaceIdWrapper::LayerSurface(id) => { tasks.push(iced_winit::wayland::commands::layer_surface::destroy_layer_surface::(id).discard()); } SurfaceIdWrapper::Popup(id) => { tasks.push( iced_winit::wayland::commands::popup::destroy_popup::(id) .discard(), ); } SurfaceIdWrapper::Subsurface(id) => { tasks.push( iced_winit::wayland::commands::subsurface::destroy_subsurface::( id, ) .discard(), ); } _ => {} } } let on_result_message = (self.on_result)(result); tasks.push(Task::future(async move { cosmic::action::app(on_result_message) })); tasks.push(command); return Task::batch(tasks); } let on_result_message = (self.on_result)(result); Task::batch([ command, Task::future(async move { cosmic::action::app(on_result_message) }), ]) } else { command } } pub fn view(&self, window_id: window::Id) -> Element<'_, M> { self.cosmic .view(window_id) .map(DialogMessage) .map(self.mapper) } pub const fn window_id(&self) -> window::Id { self.cosmic.app.flags.window_id } #[cfg(feature = "wayland")] pub fn contains_surface(&self, id: &window::Id) -> bool { self.cosmic.surface_views.contains_key(id) } } #[derive(Clone, Debug)] enum DialogPage { NewFolder { parent: PathBuf, name: String }, Replace { filename: String }, } #[derive(Clone, Debug)] struct Flags { kind: DialogKind, path_opt: Option, window_id: window::Id, #[allow(dead_code)] config_handler: Option, config: Config, } /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] enum Message { None, Cancel, Choice(usize, usize), Config(Config), DialogCancel, DialogComplete, DialogUpdate(DialogPage), Escape, Filename(String), Filter(usize), Key(Modifiers, Key, Option), ModifiersChanged(Modifiers), MounterItems(MounterKey, MounterItems), Mouse(window::Id, mouse::Button), NewFolder, NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), Open, Preview, Save(bool), ScrollTab(i16), SearchActivate, SearchClear, SearchInput(String), Surface(cosmic::surface::Action), #[allow(clippy::enum_variant_names)] TabMessage(tab::Message), TabRescan( Location, Option>, Vec, Option>, ), TabView(tab::View), TimeConfigChange(TimeConfig), ToggleFoldersFirst, ToggleShowHidden, ZoomDefault, ZoomIn, ZoomOut, } impl From for Message { fn from(app_message: AppMessage) -> Self { match app_message { AppMessage::None => Self::None, AppMessage::Preview(_entity_opt) => Self::Preview, AppMessage::SearchActivate => Self::SearchActivate, AppMessage::ScrollTab(scroll_speed) => Self::ScrollTab(scroll_speed), AppMessage::TabMessage(_entity_opt, tab_message) => Self::TabMessage(tab_message), AppMessage::TabView(_entity_opt, view) => Self::TabView(view), AppMessage::ToggleFoldersFirst => Self::ToggleFoldersFirst, AppMessage::ToggleShowHidden => Self::ToggleShowHidden, AppMessage::ZoomDefault(_entity_opt) => Self::ZoomDefault, AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn, AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut, AppMessage::NewItem(_entity_opt, true) => Self::NewFolder, AppMessage::Surface(action) => Self::Surface(action), unsupported => { log::warn!("{unsupported:?} not supported in dialog mode"); Self::None } } } } pub struct MounterData(MounterKey, MounterItem); struct WatcherWrapper { watcher_opt: Option>, } impl Clone for WatcherWrapper { fn clone(&self) -> Self { Self { watcher_opt: None } } } impl fmt::Debug for WatcherWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WatcherWrapper").finish() } } impl PartialEq for WatcherWrapper { fn eq(&self, _other: &Self) -> bool { false } } /// The [`App`] stores application-specific state. struct App { core: Core, flags: Flags, title: String, accept_label: DialogLabel, choices: Vec, context_menu_window: Option, context_page: ContextPage, dialog_pages: VecDeque, dialog_text_input: widget::Id, filters: Vec, filter_selected: Option, filename_id: widget::Id, modifiers: Modifiers, mounter_items: FxHashMap, nav_model: segmented_button::SingleSelectModel, result_opt: Option, search_id: widget::Id, tab: Tab, key_binds: HashMap, watcher_opt: Option<( Debouncer, FxHashSet, )>, auto_scroll_speed: Option, type_select_prefix: String, type_select_last_key: Option, } impl App { fn button_view(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxxs, space_xxs, space_xs, space_s, space_l, .. } = theme::spacing(); let is_condensed = self.core().is_condensed(); let mut col = widget::column::with_capacity(2).spacing(space_xxs); if let DialogKind::SaveFile { filename } = &self.flags.kind { 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)), ); } let mut row = widget::row::with_capacity( usize::from(!self.filters.is_empty()) + self.choices.len() * 2 + if is_condensed { 0 } else { 3 }, ) .align_y(Alignment::Center) .spacing(space_xxs); if !self.filters.is_empty() { row = row.push(widget::dropdown( &self.filters, self.filter_selected, Message::Filter, )); } for (choice_i, choice) in self.choices.iter().enumerate() { match choice { DialogChoice::CheckBox { label, value, .. } => { row = row.push( widget::checkbox(*value) .label(label) .on_toggle(move |checked| { Message::Choice(choice_i, usize::from(checked)) }), ); } DialogChoice::ComboBox { label, options, selected, .. } => { row = row.push(widget::text::heading(label)); row = row.push(widget::dropdown(options, *selected, move |option_i| { Message::Choice(choice_i, option_i) })); } } } if is_condensed { col = col.push(row); row = widget::row::with_capacity(3) .align_y(Alignment::Center) .spacing(space_xxs); } row = row.push(widget::space::horizontal()); row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel)); let mut has_selected = false; if let Some(items) = self.tab.items_opt() { for item in items { if item.selected { has_selected = true; break; } } } row = row.push( //TODO: easier way to create buttons with rich text widget::button::custom( widget::row::with_children([Element::from(&self.accept_label)]) .padding([0, space_s]) .width(Length::Shrink) .height(space_l) .spacing(space_xxxs) .align_y(Alignment::Center) ) .padding(0) .on_press_maybe(if self.flags.kind.save() { if let DialogKind::SaveFile { filename } = &self.flags.kind { (!filename.is_empty()).then_some(Message::Save(false)) } else { None } } else if has_selected || self.flags.kind.is_dir() { Some(Message::Open) } else { None }) .class(widget::button::ButtonClass::Suggested) /*TODO: a11y feature: .label(&self.accept_label.text)*/ ); col = col.push(row); widget::layer_container(col) .layer(cosmic_theme::Layer::Primary) .padding([8, space_xs]) .into() } fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> { let military_time = self.tab.config.military_time; let mut children = Vec::with_capacity(1); match kind { PreviewKind::Custom(PreviewItem(item)) => { children.push(item.preview_view(None, military_time)); } PreviewKind::Location(location) => { if let Some(items) = self.tab.items_opt() { for item in items { if item.location_opt.as_ref() == Some(location) { children.push(item.preview_view(None, military_time)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; } } } } PreviewKind::Selected => { if let Some(items) = self.tab.items_opt() { let preview_opt = { let mut selected = items.iter().filter(|item| item.selected); match (selected.next(), selected.next()) { // At least two selected items (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 _ => None, } }; if let Some(preview) = preview_opt { children.push(preview); } if children.is_empty() && let Some(item) = &self.tab.parent_item_opt { children.push(item.preview_view(None, military_time)); } } } } widget::column::with_children(children).into() } fn rescan_tab(&self, selection_paths: Option>) -> Task { let location = self.tab.location.clone(); let icon_sizes = self.tab.config.icon_sizes; 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)) => { #[cfg(feature = "gvfs")] { let mounter_paths: Box<[_]> = mounter_items .values() .flatten() .filter_map(MounterItem::path) .collect(); if !mounter_paths.is_empty() { for item in &mut items { item.is_mount_point = item.path_opt().is_some_and(|p| mounter_paths.contains(p)); } } } cosmic::action::app(Message::TabRescan( location, parent_item_opt, items, selection_paths, )) } Err(err) => { log::warn!("failed to rescan: {err}"); cosmic::action::none() } } }) } fn search_get(&self) -> Option<&str> { match &self.tab.location { Location::Search(_, term, ..) => Some(term), _ => None, } } fn search_set(&mut self, term_opt: Option) -> Task { let location_opt = match term_opt { Some(term) => { let search_location = if let Some(path) = self.tab.location.path_opt() { Some(SearchLocation::Path(path.clone())) } else if self.tab.location.is_recents() { Some(SearchLocation::Recents) } else if self.tab.location.is_trash() { Some(SearchLocation::Trash) } else { None }; search_location.map(|search_location| { ( Location::Search( search_location, term, self.tab.config.show_hidden, Instant::now(), ), true, ) }) } None => match &self.tab.location { Location::Search(search_location, ..) => match search_location { SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)), SearchLocation::Recents => Some((Location::Recents, false)), SearchLocation::Trash => Some((Location::Trash, false)), }, _ => None, }, }; if let Some((location, focus_search)) = location_opt { self.tab.change_location(&location, None); return Task::batch([ self.update_title(), self.update_watcher(), self.rescan_tab(None), if focus_search { widget::text_input::focus(self.search_id.clone()) } else { Task::none() }, ]); } Task::none() } fn update_config(&mut self) -> Task { self.core.window.show_context = self.flags.config.dialog.show_details; self.tab.config = self.flags.config.dialog_tab(); self.update_nav_model(); self.update(Message::TabMessage(tab::Message::Config(self.tab.config))) } fn with_dialog_config(&mut self, f: F) -> Task { let mut dialog = self.flags.config.dialog; f(&mut dialog); if dialog == self.flags.config.dialog { Task::none() } else { if let Some(config_handler) = &self.flags.config_handler { match self.flags.config.set_dialog(config_handler, dialog) { Ok(_) => {} Err(err) => { log::warn!("failed to save config \"dialog\": {err}"); } } } else { self.flags.config.dialog = dialog; log::warn!("failed to save config \"dialog\": no config handler",); } self.update_config() } } fn activate_nav_model_location(&mut self, location: &Location) { let nav_bar_id = self.nav_model.iter().find(|&id| { self.nav_model .data::(id) .is_some_and(|l| l == location) }); if let Some(id) = nav_bar_id { self.nav_model.activate(id); } else { let active = self.nav_model.active(); segmented_button::Selectable::deactivate(&mut self.nav_model, active); } } fn close_context_menus(&mut self) -> Task { self.tab.location_context_menu_index = None; if self.tab.context_menu.is_some() { return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None))); } Task::none() } fn update_nav_model(&mut self) { let mut nav_model = segmented_button::ModelBuilder::default(); 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 { continue; }; nav_model = nav_model.insert(move |b| { b.text(name.clone()) .icon( widget::icon::icon(if path.is_dir() { tab::folder_icon_symbolic(&path, 16) } else { widget::icon::from_name("text-x-generic-symbolic") .size(16) .handle() }) .size(16), ) .data(Location::Path(path.clone())) }); } } // Collect all mounter items let mut nav_items = Vec::new(); for (key, items) in &self.mounter_items { nav_items.extend(items.iter().map(|item| (*key, item))); } // Sort by name lexically nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name())); // Add items to nav model for (i, (key, item)) in nav_items.into_iter().enumerate() { nav_model = nav_model.insert(|mut b| { b = b.text(item.name()).data(MounterData(key, item.clone())); if let Some(path) = item.path() { b = b.data(Location::Path(path)); } if let Some(icon) = item.icon(true) { b = b.icon(widget::icon::icon(icon).size(16)); } if item.is_mounted() { b = b.closable(); } if i == 0 { b = b.divider_above(); } b }); } self.nav_model = nav_model.build(); self.activate_nav_model_location(&self.tab.location.clone()); } fn update_title(&mut self) -> Task { self.set_header_title(self.title.clone()); self.set_window_title(self.title.clone(), self.flags.window_id) } fn update_watcher(&mut self) -> Task { if let Some((mut watcher, old_paths)) = self.watcher_opt.take() { let mut new_paths = FxHashSet::default(); if let Some(path) = &self.tab.location.path_opt() { new_paths.insert((*path).clone()); } // Unwatch paths no longer used for path in &old_paths { if !new_paths.contains(path) { match watcher.unwatch(path) { Ok(()) => { log::debug!("unwatching {}", path.display()); } Err(err) => { log::debug!("failed to unwatch {}: {}", path.display(), err); } } } } // Watch new paths for path in &new_paths { if !old_paths.contains(path) { //TODO: should this be recursive? match watcher.watch(path, notify::RecursiveMode::NonRecursive) { Ok(()) => { log::debug!("watching {}", path.display()); } Err(err) => { log::debug!("failed to watch {}: {}", path.display(), err); } } } } self.watcher_opt = Some((watcher, new_paths)); } //TODO: should any of this run in a command? Task::none() } } /// Implement [`Application`] to integrate with COSMIC. impl Application for App { /// Default async executor to use with the app. type Executor = executor::Default; /// Argument received type Flags = Flags; /// Message type specific to our [`App`]. type Message = Message; /// The unique application ID to supply to the window manager. const APP_ID: &'static str = "com.system76.CosmicFilesDialog"; fn core(&self) -> &Core { &self.core } fn core_mut(&mut self) -> &mut Core { &mut self.core } /// Creates the application, and optionally emits command on initialize. fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { core.window.context_is_overlay = false; core.window.show_close = false; core.window.show_maximize = false; core.window.show_minimize = false; let title = flags.kind.title(); let accept_label = flags.kind.accept_label(); let location = Location::Path(match &flags.path_opt { Some(path) => path.clone(), None => match env::current_dir() { Ok(path) => path, Err(_) => home_dir(), }, }); let mut tab = Tab::new( location, flags.config.dialog_tab(), ThumbCfg::default(), None, widget::Id::unique(), None, ); tab.mode = tab::Mode::Dialog(flags.kind.clone()); tab.sort_name = tab::HeadingOptions::Modified; tab.sort_direction = false; let key_binds = key_binds(&tab.mode); let mut app = Self { core, flags, title, accept_label: DialogLabel::from(accept_label), choices: Vec::new(), context_menu_window: None, context_page: ContextPage::Preview(None, PreviewKind::Selected), dialog_pages: VecDeque::new(), dialog_text_input: widget::Id::new("Dialog Text Input"), filters: Vec::new(), filter_selected: None, filename_id: widget::Id::new("Dialog Filename"), modifiers: Modifiers::empty(), mounter_items: FxHashMap::default(), nav_model: segmented_button::ModelBuilder::default().build(), result_opt: None, search_id: widget::Id::new("Dialog File Search"), tab, key_binds, watcher_opt: None, auto_scroll_speed: None, type_select_prefix: String::new(), type_select_last_key: None, }; let commands = Task::batch([ app.update_config(), app.update_title(), app.update_watcher(), app.rescan_tab(None), ]); (app, commands) } fn context_drawer(&self) -> Option> { if !self.core.window.show_context { return None; } match &self.context_page { ContextPage::Preview(_, kind) => { let actions = self .tab .items_opt() .and_then(|items| { items .iter() .find(|item| item.selected) .map(|item| item.preview_actions().map(Message::TabMessage)) }) .unwrap_or_else(|| widget::space::horizontal().into()); Some( context_drawer::context_drawer( self.preview(kind).map(Message::TabMessage), Message::Preview, ) .actions(actions), ) } _ => None, } } fn dialog(&self) -> Option> { let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); //TODO: should gallery view just be a dialog? if self.tab.gallery { return Some( widget::column::with_children([ self.tab.gallery_view().map(Message::TabMessage), // Draw button row as part of the overlay widget::container(self.button_view()) .width(Length::Fill) .padding(space_xxs) .class(theme::Container::WindowBackground) .into(), ]) .into(), ); } let dialog_page = self.dialog_pages.front()?; let dialog = match dialog_page { DialogPage::NewFolder { parent, name } => { let mut dialog = widget::dialog().title(fl!("create-new-folder")); let complete_maybe = if name.is_empty() { None } else if name == "." || name == ".." { dialog = dialog.tertiary_action(widget::text::body(fl!( "name-invalid", filename = name.as_str() ))); None } else if name.contains('/') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); None } else { let path = parent.join(name); if path.exists() { if path.is_dir() { dialog = dialog .tertiary_action(widget::text::body(fl!("folder-already-exists"))); } else { dialog = dialog .tertiary_action(widget::text::body(fl!("file-already-exists"))); } None } else { if name.starts_with('.') { dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); } Some(Message::DialogComplete) } }; dialog .primary_action( widget::button::suggested(fl!("save")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) .control( widget::column::with_children([ widget::text::body(fl!("folder-name")).into(), widget::text_input("", name.as_str()) .id(self.dialog_text_input.clone()) .on_input(move |name| { Message::DialogUpdate(DialogPage::NewFolder { parent: parent.clone(), name, }) }) .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) .into(), ]) .spacing(space_xxs), ) } DialogPage::Replace { filename } => widget::dialog() .title(fl!("replace-title", filename = filename.as_str())) .icon(widget::icon::from_name("dialog-question").size(64)) .body(fl!("replace-warning")) .primary_action( 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), ), }; Some(dialog.into()) } fn footer(&self) -> Option> { Some(self.button_view()) } fn header_end(&self) -> Vec> { let mut elements = Vec::with_capacity(3); if let Some(term) = self.search_get() { if self.core.is_condensed() { elements.push( //TODO: selected state is not appearing different widget::button::icon(widget::icon::from_name("system-search-symbolic")) .on_press(Message::SearchClear) .padding(8) .selected(true) .into(), ); } else { elements.push( widget::text_input::search_input("", term) .width(Length::Fixed(240.0)) .id(self.search_id.clone()) .on_clear(Message::SearchClear) .on_input(Message::SearchInput) .into(), ); } } else { elements.push( widget::button::icon(widget::icon::from_name("system-search-symbolic")) .on_press(Message::SearchActivate) .padding(8) .into(), ); } if self.flags.kind.save() { elements.push( widget::button::icon(widget::icon::from_name("folder-new-symbolic")) .on_press(Message::NewFolder) .padding(8) .into(), ); } let show_details = match self.context_page { ContextPage::Preview(..) => self.core.window.show_context, _ => false, }; elements .push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from)); elements } fn nav_bar(&self) -> Option>> { if !self.core().nav_bar_active() { return None; } let nav_model = self.nav_model()?; let mut nav = cosmic::widget::nav_bar(nav_model, |entity| { cosmic::action::cosmic(cosmic::app::Action::NavBar(entity)) }) //TODO .on_close(|entity| cosmic::cosmic::action::app(Message::NavBarClose(entity))) .close_icon( widget::icon::from_name("media-eject-symbolic") .size(16) .icon(), ) .into_container(); if !self.core().is_condensed() { nav = nav.max_width(280); } Some(Element::from( nav.width(Length::Shrink).height(Length::Fill), )) } fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> { Some(&self.nav_model) } fn on_app_exit(&mut self) -> Option { self.result_opt = Some(DialogResult::Cancel); None } fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Task { self.nav_model.activate(entity); if let Some(location) = self.nav_model.data::(entity) { let message = Message::TabMessage(tab::Message::Location(location.clone())); return self.update(message); } if let Some(data) = self.nav_model.data::(entity) && let Some(mounter) = MOUNTERS.get(&data.0) { return mounter .mount(data.1.clone()) .map(|()| cosmic::action::none()); } Task::none() } fn on_escape(&mut self) -> Task { if self.tab.gallery { // Close gallery if open self.tab.gallery = false; return Task::none(); } if self.tab.location_context_menu_index.is_some() { self.tab.location_context_menu_index = None; return Task::none(); } if self.tab.context_menu.is_some() { return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None))); } if self.tab.edit_location.is_some() { // Close location editing if enabled self.tab.edit_location = None; return Task::none(); } if self.search_get().is_some() { // Close search if open return self.search_set(None); } let had_focused_button = self.tab.select_focus_id().is_some(); if self.tab.select_none() { if had_focused_button { // Unfocus if there was a focused button return widget::button::focus(widget::Id::unique()); } return Task::none(); } // Close the dialog if the focused widget is the dialog's main text input instead of // unfocussing the widget. if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish() && self.dialog_text_input == focused { return self.update(Message::Cancel); } self.update(Message::Cancel) } /// Handle application events here. fn update(&mut self, message: Message) -> Task { match message { Message::None => {} Message::Cancel => { self.result_opt = Some(DialogResult::Cancel); return window::close(self.flags.window_id); } Message::Choice(choice_i, option_i) => { if let Some(choice) = self.choices.get_mut(choice_i) { match choice { DialogChoice::CheckBox { value, .. } => *value = option_i > 0, DialogChoice::ComboBox { options, selected, .. } => { if option_i < options.len() { *selected = Some(option_i); } else { *selected = None; } } } } } Message::Config(config) => { if config != self.flags.config { log::info!("update config"); self.flags.config = config; return self.update_config(); } } Message::DialogCancel => { self.dialog_pages.pop_front(); } Message::DialogComplete => { if let Some(dialog_page) = self.dialog_pages.pop_front() { match dialog_page { DialogPage::NewFolder { parent, name } => { let path = parent.join(name); match fs::create_dir(&path) { Ok(()) => { // cd to directory let message = Message::TabMessage(tab::Message::Location( Location::Path(path), )); return self.update(message); } Err(err) => { log::warn!("failed to create {}: {}", path.display(), err); } } } DialogPage::Replace { .. } => { return self.update(Message::Save(true)); } } } } Message::DialogUpdate(dialog_page) => { if !self.dialog_pages.is_empty() { self.dialog_pages[0] = dialog_page; } } Message::Escape => return self.on_escape(), Message::Filename(new_filename) => { // Select based on filename self.tab.select_name(&new_filename); if let DialogKind::SaveFile { filename } = &mut self.flags.kind { *filename = new_filename; } } Message::Filter(filter_i) => { if filter_i < self.filters.len() { self.filter_selected = Some(filter_i); } else { self.filter_selected = None; } return self.rescan_tab(None); } Message::Key(modifiers, key, text) => { for (key_bind, action) in &self.key_binds { if key_bind.matches(modifiers, &key) { 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) { return self.update(if self.flags.kind.save() { Message::Save(false) } else { Message::Open }); } // Uncaptured keys with only shift modifiers go to the search or location box if !modifiers.logo() && !modifiers.control() && !modifiers.alt() && matches!(key, Key::Character(_)) && let Some(text) = text { match self.flags.config.type_to_search { TypeToSearch::Recursive => { let mut term = self.search_get().unwrap_or_default().to_string(); term.push_str(&text); return self.search_set(Some(term)); } TypeToSearch::EnterPath => { let location = (self.tab.edit_location) .as_ref() .map_or_else(|| &self.tab.location, |x| &x.location); // Try to add text to end of location if let Some(path) = location.path_opt() { let mut path_string = path.to_string_lossy().to_string(); path_string.push_str(&text); self.tab.edit_location = Some(location.with_path(PathBuf::from(path_string)).into()); } } TypeToSearch::SelectByPrefix => { // Reset buffer if timeout elapsed if let Some(last_key) = self.type_select_last_key && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { self.type_select_prefix.clear(); } // Accumulate character and select self.type_select_prefix.push_str(&text.to_lowercase()); self.type_select_last_key = Some(Instant::now()); self.tab.select_by_prefix(&self.type_select_prefix); if let Some(offset) = self.tab.select_focus_scroll() { return scrollable::scroll_to( self.tab.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ); } } } } } Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders let mut unmounted = Vec::new(); if let Some(old_items) = self.mounter_items.get(&mounter_key) { for old_item in old_items { if let Some(old_path) = old_item.path() && old_item.is_mounted() { let mut still_mounted = false; for item in &mounter_items { if let Some(path) = item.path() && path == old_path && item.is_mounted() { still_mounted = true; break; } } if !still_mounted { unmounted.push(Location::Path(old_path)); } } } } // Go back to home in any tabs that were unmounted let mut commands = Vec::new(); { let home_location = Location::Path(home_dir()); if unmounted.contains(&self.tab.location) { self.tab.change_location(&home_location, None); commands.push(self.update_watcher()); commands.push(self.rescan_tab(None)); } } // Insert new items self.mounter_items.insert(mounter_key, mounter_items); // Update nav bar //TODO: this could change favorites IDs while they are in use self.update_nav_model(); return Task::batch(commands); } Message::Mouse(window_id, _button) => { // Close context menu when clicking outside. if self.core.main_window_id() == Some(window_id) { return self.close_context_menus(); } } Message::NewFolder => { if let Some(path) = self.tab.location.path_opt() { self.dialog_pages.push_back(DialogPage::NewFolder { parent: path.clone(), name: String::new(), }); return widget::text_input::focus(self.dialog_text_input.clone()); } } Message::NotifyEvents(events) => { log::debug!("{events:?}"); if let Some(path) = self.tab.location.path_opt() { let mut contains_change = false; for event in &events { for event_path in &event.paths { if event_path.starts_with(path) { if let notify::EventKind::Modify( notify::event::ModifyKind::Metadata(_) | notify::event::ModifyKind::Data(_), ) = event.kind { // If metadata or data changed, find the matching item and reload it //TODO: this could be further optimized by looking at what exactly changed if let Some(items) = &mut self.tab.items_opt { for item in items.iter_mut() { if item.path_opt() == Some(event_path) { //TODO: reload more, like mime types? match fs::metadata(event_path) { Ok(new_metadata) => { if let ItemMetadata::Path { metadata, .. } = &mut item.metadata { *metadata = new_metadata; } } Err(err) => { log::warn!( "failed to reload metadata for {}: {}", path.display(), err ); } } //TODO item.thumbnail_opt = } } } } else { // Any other events reload the whole tab contains_change = true; break; } } } } if contains_change { return self.rescan_tab(None); } } } Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take() { Some(watcher) => { self.watcher_opt = Some((watcher, FxHashSet::default())); return self.update_watcher(); } None => { log::warn!("message did not contain notify watcher"); } }, Message::Open => { let mut paths = Vec::new(); if let Some(items) = self.tab.items_opt() { for item in items { if item.selected && let Some(path) = item.path_opt() { paths.push(path.clone()); if self.flags.config.show_recents { let _ = update_recently_used( path, Self::APP_ID.to_string(), "cosmic-files".to_string(), None, ); } } } } // Ensure selection is allowed //TODO: improve tab logic so this doesn't block the open button so often for path in &paths { let path_is_dir = path.is_dir(); if path_is_dir != self.flags.kind.is_dir() { if path_is_dir && paths.len() == 1 { // If the only selected item is a directory and we are selecting files, cd to it let message = Message::TabMessage(tab::Message::Location( Location::Path(path.clone()), )); return self.update(message); } // Otherwise, this is not a legal selection return Task::none(); } } // If there are proper matching items, return them if !paths.is_empty() { self.result_opt = Some(DialogResult::Open(paths)); return window::close(self.flags.window_id); } // If we are in directory mode, return the current directory if self.flags.kind.is_dir() && let Location::Path(tab_path) = &self.tab.location { self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()])); return window::close(self.flags.window_id); } } Message::Preview => { self.context_page = ContextPage::Preview(None, PreviewKind::Selected); return self.with_dialog_config(|config| { config.show_details = !config.show_details; }); } Message::Save(replace) => { if let DialogKind::SaveFile { filename } = &self.flags.kind && !filename.is_empty() && let Some(tab_path) = self.tab.location.path_opt() { let path = tab_path.join(filename); if path.is_dir() { // cd to directory let message = Message::TabMessage(tab::Message::Location(Location::Path(path))); return self.update(message); } else if !replace && path.exists() { self.dialog_pages.push_back(DialogPage::Replace { filename: filename.clone(), }); return widget::button::focus(REPLACE_BUTTON_ID.clone()); } self.result_opt = Some(DialogResult::Open(vec![path])); return window::close(self.flags.window_id); } } Message::ScrollTab(scroll_speed) => { return self.update(Message::TabMessage(tab::Message::ScrollTab( f32::from(scroll_speed) / 10.0, ))); } Message::SearchActivate => { let mut tasks = vec![self.close_context_menus()]; if self.search_get().is_none() { tasks.push(self.search_set(Some(String::new()))); } else { tasks.push(widget::text_input::focus(self.search_id.clone())); } return Task::batch(tasks); } Message::SearchClear => { return Task::batch([self.close_context_menus(), self.search_set(None)]); } Message::SearchInput(input) => { return self.search_set(Some(input)); } Message::TabMessage(tab_message) => { let click_i_opt = match tab_message { tab::Message::Click(click_i_opt) => click_i_opt, _ => None, }; let tab_commands = self.tab.update(tab_message, self.modifiers); // Update filename box when anything is selected if let DialogKind::SaveFile { filename } = &mut self.flags.kind && let Some(click_i) = click_i_opt && let Some(items) = self.tab.items_opt() && let Some(item) = items.get(click_i) && item.selected && !item.metadata.is_dir() { filename.clone_from(&item.name); } let mut commands = Vec::new(); for tab_command in tab_commands { match tab_command { tab::Command::Action(action) => { commands.push(self.update(Message::from(action.message()))); } tab::Command::ChangeLocation(_tab_title, _tab_path, selection_paths) => { commands.push(Task::batch([ self.update_watcher(), self.rescan_tab(selection_paths), ])); } tab::Command::ContextMenu(point_opt, parent_id) => { #[cfg(feature = "wayland")] match point_opt { Some(point) => { if crate::is_wayland() { // Open context menu use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ Anchor, Gravity, }; use cosmic::iced::runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; use cosmic::iced::Rectangle; let window_id = window::Id::unique(); self.context_menu_window = Some(window_id); let autosize_id = widget::Id::unique(); commands.push(self.update(Message::Surface( cosmic::surface::action::app_popup( move |app: &mut Self| -> SctkPopupSettings { let anchor_rect = Rectangle { x: point.x as i32, y: point.y as i32, width: 1, height: 1, }; let positioner = SctkPositioner { size: None, anchor_rect, anchor: Anchor::None, gravity: Gravity::BottomRight, reactive: true, ..Default::default() }; SctkPopupSettings { parent: parent_id .unwrap_or(app.flags.window_id), id: window_id, positioner, parent_size: None, grab: true, close_with_children: false, input_zone: None, } }, Some(Box::new(move |app: &Self| { widget::autosize::autosize( menu::context_menu( &app.tab, &app.key_binds, &app.modifiers, false, // Paste not used in dialogs &app.flags.config.context_actions, ) .map(Message::TabMessage) .map(cosmic::Action::App), autosize_id.clone(), ) .into() })), ), ))); } } None => { if let Some(window_id) = self.context_menu_window.take() { commands.push(self.update(Message::Surface( cosmic::surface::action::destroy_popup(window_id), ))); } } } } tab::Command::Iced(iced_command) => { commands.push(iced_command.0.map(|tab_message| { cosmic::action::app(Message::TabMessage(tab_message)) })); } tab::Command::OpenFile(_item_path) => { if self.flags.kind.save() { commands.push(self.update(Message::Save(false))); } else { commands.push(self.update(Message::Open)); } } tab::Command::Preview(kind) => { self.context_page = ContextPage::Preview(None, kind); commands.push(self.with_dialog_config(|config| { config.show_details = true; })); } tab::Command::WindowDrag => { commands.push(window::drag(self.flags.window_id)); } tab::Command::WindowToggleMaximize => { commands.push(window::toggle_maximize(self.flags.window_id)); } tab::Command::AutoScroll(scroll_speed) => { // converting an f32 to an i16 here by multiplying by 10 and casting to i16 // further resolution isn't necessary if let Some(scroll_speed_float) = scroll_speed { self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16); } else { self.auto_scroll_speed = None; } } unsupported => { log::warn!("{unsupported:?} not supported in dialog mode"); } } } return Task::batch(commands); } Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => { if location == self.tab.location { // Filter if let Some(filter_i) = self.filter_selected && let Some(filter) = self.filters.get(filter_i) { let mut parsed_globs = Vec::new(); let mut mimes = Vec::new(); for pattern in &filter.patterns { match pattern { DialogFilterPattern::Glob(value) => { match glob::Pattern::new(value) { Ok(glob) => parsed_globs.push(glob), Err(err) => { log::warn!("failed to parse glob {value:?}: {err}"); } } } DialogFilterPattern::Mime(value) => match value.parse::() { Ok(parsed) => mimes.push(parsed), Err(err) => { log::warn!("failed to parse mime {value:?}: {err}"); } }, } } items.retain(|item| { // Directories are always shown item.metadata.is_dir() || mimes.iter().any(|filter_mime| { if filter_mime.subtype() == mime::STAR { filter_mime.type_() == item.mime.type_() } else { *filter_mime == item.mime } }) // Check for glob match (last because it is slower) || parsed_globs.iter().any(|glob| glob.matches(&item.name)) }); } // Select based on filename if let DialogKind::SaveFile { filename } = &self.flags.kind { for item in &mut items { item.selected = &item.name == filename; } } self.tab.parent_item_opt = parent_item_opt; self.tab.set_items(items); if let Some(mut selection_paths) = selection_paths { if !self.flags.kind.multiple() { selection_paths.truncate(1); } self.tab.select_paths(selection_paths); } // Reset focus on location change 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()); } } Message::TabView(view) => { return self.with_dialog_config(|config| { config.view = view; }); } Message::TimeConfigChange(time_config) => { self.flags.config.tab.military_time = time_config.military_time; return self.update_config(); } Message::ToggleFoldersFirst => { return self.with_dialog_config(|config| { config.folders_first = !config.folders_first; }); } Message::ToggleShowHidden => { return self.with_dialog_config(|config| { config.show_hidden = !config.show_hidden; }); } Message::ZoomDefault => { return self.with_dialog_config(|config| { zoom_to_default(config.view, &mut config.icon_sizes); }); } Message::ZoomIn => { return self.with_dialog_config(|config| { zoom_in_view(config.view, &mut config.icon_sizes); }); } Message::ZoomOut => { return self.with_dialog_config(|config| { zoom_out_view(config.view, &mut config.icon_sizes); }); } Message::Surface(action) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(action), )); } } Task::none() } /// Creates a view after each update. fn view(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); let mut col = widget::column::with_capacity(2); if self.core.is_condensed() && let Some(term) = self.search_get() { col = col.push( widget::container( widget::text_input::search_input("", term) .width(Length::Fill) .id(self.search_id.clone()) .on_clear(Message::SearchClear) .on_input(Message::SearchInput), ) .padding(space_xxs), ); } col = col.push( self.tab .view(&self.key_binds, &self.modifiers, false, &[]) .map(Message::TabMessage), ); col.into() } fn subscription(&self) -> Subscription { struct WatcherSubscription; struct TimeSubscription; let mut subscriptions = vec![ event::listen_with(|event, status, window_id| match event { Event::Mouse(mouse::Event::ButtonPressed(button)) => match status { event::Status::Ignored => Some(Message::Mouse(window_id, button)), event::Status::Captured => None, }, Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, text, .. }) => match status { event::Status::Ignored => Some(Message::Key(modifiers, key, text)), event::Status::Captured => { if key == Key::Named(Named::Escape) { Some(Message::Escape) } else { None } } }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::ModifiersChanged(modifiers)) } _ => None, }), Config::subscription().map(|update| { if !update.errors.is_empty() { log::info!( "errors loading config {:?}: {:?}", update.keys, update.errors ); } Message::Config(update.config) }), cosmic_config::config_subscription::<_, TimeConfig>( TypeId::of::(), TIME_CONFIG_ID.into(), 1, ) .map(|update| { if !update.errors.is_empty() { log::info!( "errors loading time config {:?}: {:?}", update.keys, update.errors ); } Message::TimeConfigChange(update.config) }), Subscription::run_with(TypeId::of::(), |_| { stream::channel(100, { |mut output: futures::channel::mpsc::Sender<_>| async move { let watcher_res = { let mut output = output.clone(); new_debouncer( time::Duration::from_millis(250), Some(time::Duration::from_millis(250)), move |events_res: notify_debouncer_full::DebounceEventResult| { match events_res { Ok(mut events) => { events.retain(|event| { match &event.kind { notify::EventKind::Access(_) => { // Data not mutated false } notify::EventKind::Modify( notify::event::ModifyKind::Metadata(e), ) if (*e != notify::event::MetadataKind::Any && *e != notify::event::MetadataKind::WriteTime) => { // Data not mutated nor modify time changed false } _ => true } }); if !events.is_empty() { match futures::executor::block_on(async { output.send(Message::NotifyEvents(events)).await }) { Ok(()) => {} Err(err) => { log::warn!( "failed to send notify events: {err:?}" ); } } } } Err(err) => { log::warn!("failed to watch files: {err:?}"); } } }, ) }; match watcher_res { Ok(watcher) => { match output .send(Message::NotifyWatcher(WatcherWrapper { watcher_opt: Some(watcher), })) .await { Ok(()) => {} Err(err) => { log::warn!("failed to send notify watcher: {err:?}"); } } } Err(err) => { log::warn!("failed to create file watcher: {err:?}"); } } std::future::pending().await } }) }), self.tab .subscription( self.core.window.show_context && matches!( self.context_page, ContextPage::Preview(_, PreviewKind::Selected) ), ) .map(Message::TabMessage), ]; if let Some(scroll_speed) = self.auto_scroll_speed { subscriptions.push( iced::time::every(time::Duration::from_millis(10)) .with(scroll_speed) .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)), ); } subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| { mounter .subscription() .with(*key) .map(|(key, mounter_message)| { if let MounterMessage::Items(items) = mounter_message { Message::MounterItems(key, items) } else { log::warn!("{mounter_message:?} not supported in dialog mode"); Message::None } }) })); Subscription::batch(subscriptions) } } ================================================ FILE: src/key_bind.rs ================================================ 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; use crate::tab; //TODO: load from config pub fn key_binds(mode: &tab::Mode) -> HashMap { let mut key_binds = HashMap::new(); macro_rules! bind { ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{ key_binds.insert( KeyBind { modifiers: vec![$(Modifier::$modifier),*], key: $key, }, Action::$action, ); }}; } // Common keys bind!([], Key::Named(Named::ArrowDown), ItemDown); bind!([], Key::Named(Named::ArrowLeft), ItemLeft); bind!([], Key::Named(Named::ArrowRight), ItemRight); bind!([], Key::Named(Named::ArrowUp), ItemUp); bind!([], Key::Named(Named::F5), Reload); bind!([], Key::Named(Named::Home), SelectFirst); bind!([], Key::Named(Named::End), SelectLast); bind!([Shift], Key::Named(Named::ArrowDown), ItemDown); bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft); bind!([Shift], Key::Named(Named::ArrowRight), ItemRight); bind!([Shift], Key::Named(Named::ArrowUp), ItemUp); bind!([Shift], Key::Named(Named::Home), SelectFirst); bind!([Shift], Key::Named(Named::End), SelectLast); bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder); bind!([], Key::Named(Named::Enter), Open); bind!([Ctrl], Key::Character(" ".into()), Preview); bind!([], Key::Character(" ".into()), Gallery); bind!([Ctrl], Key::Character("h".into()), ToggleShowHidden); bind!([Ctrl], Key::Character("a".into()), SelectAll); bind!([Ctrl], Key::Character("=".into()), ZoomIn); bind!([Ctrl], Key::Character("+".into()), ZoomIn); bind!([Ctrl], Key::Character("0".into()), ZoomDefault); bind!([Ctrl], Key::Character("-".into()), ZoomOut); // Switch view bind!([Ctrl], Key::Character("1".into()), TabViewList); bind!([Ctrl], Key::Character("2".into()), TabViewGrid); // App-only keys if matches!(mode, tab::Mode::App) { bind!([Ctrl], Key::Character("d".into()), AddToSidebar); bind!([Ctrl], Key::Named(Named::Enter), OpenInNewTab); bind!([Ctrl], Key::Character(",".into()), Settings); bind!([Ctrl], Key::Character("w".into()), TabClose); bind!([Ctrl], Key::Character("t".into()), TabNew); bind!([Ctrl], Key::Named(Named::Tab), TabNext); bind!([Ctrl, Shift], Key::Named(Named::Tab), TabPrev); bind!([Ctrl], Key::Character("q".into()), WindowClose); bind!([Ctrl], Key::Character("n".into()), WindowNew); } // App and desktop only keys if matches!(mode, tab::Mode::App | tab::Mode::Desktop) { bind!([Ctrl], Key::Character("c".into()), Copy); bind!([Ctrl, Shift], Key::Character("c".into()), CopyPath); bind!([Ctrl], Key::Character("x".into()), Cut); bind!([], Key::Named(Named::Delete), Delete); bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete); bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow); bind!([Ctrl], Key::Character("v".into()), Paste); bind!([], Key::Named(Named::F2), Rename); } // App and dialog only keys if matches!(mode, tab::Mode::App | tab::Mode::Dialog(_)) { bind!([Ctrl], Key::Character("l".into()), EditLocation); bind!([Alt], Key::Named(Named::ArrowRight), HistoryNext); bind!([Alt], Key::Named(Named::ArrowLeft), HistoryPrevious); bind!([], Key::Named(Named::Backspace), HistoryPrevious); bind!([Alt], Key::Named(Named::ArrowUp), LocationUp); bind!([Ctrl], Key::Character("f".into()), SearchActivate); } key_binds } ================================================ FILE: src/large_image.rs ================================================ use cosmic::widget; use image::ImageReader; 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; /// System memory reserve in MB to maintain for system stability (prevents thrashing) /// Note: RAM checking is currently only available on Linux via procfs. /// On Windows and macOS, only GPU buffer limits are enforced. const SYSTEM_MEMORY_RESERVE_MB: u64 = 500; /// Maximum memory allocation for gallery image decoding in MB. /// Gallery mode uses the full memory budget since only one image decodes at a time. /// This matches the ThumbCfg max_mem_mb budget for consistency. const GALLERY_MEMORY_LIMIT_MB: u64 = 2000; /// Threshold for considering an image "large" requiring GPU tiling /// Atlas fragment/tile size in pixels. Large images are split into fragments of this size. /// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs pub const ATLAS_FRAGMENT_SIZE: u32 = 4096; /// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations) pub const MB_TO_BYTES: u64 = 1024 * 1024; /// Conversion factor: 1 MB = 1000 * 1000 bytes (decimal megabyte, used by image crate) /// The image crate's memory limits use decimal MB, not binary MB. pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000; /// Scale factor for HiDPI displays - decode at higher resolution than display size /// for better quality on high-DPI screens. 1.5x provides good balance between /// quality and memory usage and also prevets re-decoding on small windows size adjustments. const DISPLAY_SCALE_FACTOR: f32 = 1.5; /// Calculate optimal target dimensions for decoding based on display size. /// Returns None if no resizing is needed (image is smaller than display). /// /// This helps reduce memory usage by decoding large images at a resolution /// appropriate for the display, rather than always using full resolution. pub fn calculate_target_dimensions( image_width: u32, image_height: u32, display_width: u32, display_height: u32, ) -> Option<(u32, u32)> { let target_width = (display_width as f32 * DISPLAY_SCALE_FACTOR) as u32; let target_height = (display_height as f32 * DISPLAY_SCALE_FACTOR) as u32; if image_width <= target_width && image_height <= target_height { return None; } let image_aspect = image_width as f32 / image_height as f32; let target_aspect = target_width as f32 / target_height as f32; let (new_width, new_height) = if image_aspect > target_aspect { let w = target_width; let h = (target_width as f32 / image_aspect) as u32; (w, h) } else { let h = target_height; let w = (target_height as f32 * image_aspect) as u32; (w, h) }; let new_width = new_width.max(1); let new_height = new_height.max(1); log::info!( "Calculated target dimensions: {}x{} -> {}x{} (display: {}x{}, scale: {}x)", image_width, image_height, new_width, new_height, display_width, display_height, DISPLAY_SCALE_FACTOR ); Some((new_width, new_height)) } /// Check if an image's dimensions would exceed the available memory budget. /// Returns true if the image is too large to decode. pub fn exceeds_memory_limit(width: u32, height: u32, memory_limit_mb: u64) -> bool { let Some(bytes_needed) = calculate_image_memory(width, height) else { // Overflow in calculation means it definitely exceeds any reasonable limit return true; }; let max_bytes = memory_limit_mb * MB_TO_BYTES; bytes_needed > max_bytes } /// Check if an image should use GPU tiling for display. /// Images larger than the atlas fragment size need to be split into tiles for GPU upload. pub fn should_use_tiling(width: u32, height: u32) -> bool { width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE } /// Determine if an image should use the dedicated worker for thumbnail generation. /// Returns (use_dedicated_worker, effective_max_mb, effective_jobs). /// /// Large images that exceed per-worker memory budget get routed to a dedicated worker /// with full memory budget. Smaller images use the normal parallel worker pool. pub fn should_use_dedicated_worker( width: u32, height: u32, total_budget_mb: u64, parallel_workers: usize, ) -> (bool, u64, usize) { if width == 0 || height == 0 { log::warn!( "Invalid image dimensions {}x{}, using normal queue", width, height ); return (false, total_budget_mb, parallel_workers); } let Some(bytes_needed) = calculate_image_memory(width, height) else { log::warn!( "Image dimensions {}x{} overflow memory calculation, using normal queue", width, height ); return (false, total_budget_mb, parallel_workers); }; let mb_needed = bytes_needed / MB_TO_BYTES; let per_worker_budget_mb = total_budget_mb / parallel_workers as u64; if mb_needed > per_worker_budget_mb { log::info!( "Large image {}x{} needs {}MB (exceeds per-worker {}MB), using dedicated worker", width, height, mb_needed, per_worker_budget_mb ); // Use dedicated worker with full budget (true, total_budget_mb, 1) } else { log::debug!( "Normal image {}x{} needs {}MB (within per-worker {}MB), using parallel workers", width, height, mb_needed, per_worker_budget_mb ); // Use parallel worker pool with shared budget (false, total_budget_mb, parallel_workers) } } /// Get the dimensions of an image without fully decoding it pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> { match ImageReader::open(path) { Ok(reader) => match reader.into_dimensions() { Ok((width, height)) => { log::debug!( "Image dimensions: {}x{} for {}", width, height, path.display() ); Some((width, height)) } Err(e) => { log::warn!("Failed to get dimensions for {}: {}", path.display(), e); None } }, Err(e) => { log::warn!("Failed to open image reader for {}: {}", path.display(), e); None } } } /// Calculate the memory required to decode an image in bytes. /// Returns None if the calculation overflows. fn calculate_image_memory(width: u32, height: u32) -> Option { let pixels = (width as u64).checked_mul(height as u64)?; pixels.checked_mul(RGBA_BYTES_PER_PIXEL) } /// Check if there's sufficient system RAM to decode an image (Linux only). /// Returns: (has_memory, error_message) #[cfg(target_os = "linux")] fn check_ram_available(width: u32, height: u32) -> (bool, Option) { use procfs::Current; let Some(bytes_needed) = calculate_image_memory(width, height) else { let error_msg = format!( "Image dimensions too large: {}x{} causes overflow in memory calculation", width, height ); log::error!("{}", error_msg); return (false, Some(error_msg)); }; let mb_needed = bytes_needed / MB_TO_BYTES; match procfs::Meminfo::current() { Ok(meminfo) => { // MemAvailable includes reclaimable cache and is the best estimate of // actually available memory for new allocations let available_kb = meminfo.mem_available.unwrap_or(0); let available_bytes = available_kb * 1024; // Maintain system reserve to prevent thrashing and OOM killer let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES; let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes); if bytes_needed > usable_bytes { let available_mb = available_bytes / MB_TO_BYTES; let error_msg = format!( "Insufficient memory: need {}MB, available {}MB. Try closing other applications.", mb_needed, available_mb ); log::warn!("{}", error_msg); return (false, Some(error_msg)); } (true, None) } Err(e) => { log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e); // Graceful fallback: assume RAM is available (true, None) } } } #[cfg(not(target_os = "linux"))] fn check_ram_available(_width: u32, _height: u32) -> (bool, Option) { // RAM checking not implemented for this platform (true, None) } pub fn check_memory_available(width: u32, height: u32) -> (bool, Option) { if width == 0 || height == 0 { let error_msg = format!( "Invalid image dimensions: {}x{} (zero dimension)", width, height ); log::error!("{}", error_msg); return (false, Some(error_msg)); } // Check system RAM availability check_ram_available(width, height) } /// Decode a large image asynchronously in a blocking thread pool. /// /// This function is used for gallery mode where full-resolution images need to be loaded. /// It uses the full memory budget (GALLERY_MEMORY_LIMIT_MB) since only one image /// decodes at a time in gallery mode. pub async fn decode_large_image( path: PathBuf, target_dimensions: Option<(u32, u32)>, ) -> Option<(PathBuf, u32, u32, Vec)> { // Decode image in blocking thread pool (CPU-intensive work should not block) tokio::task::spawn_blocking(move || { log::info!("Starting async decode of {}", path.display()); // Use ImageReader with explicit memory limits to avoid "Memory limit exceeded" errors // Gallery mode uses the full memory budget since only one image decodes at a time match image::ImageReader::open(&path) { Ok(reader) => { match reader.with_guessed_format() { Ok(mut reader) => { // Note: image crate uses decimal MB (1000^2), not binary MB (1024^2) let mut limits = image::Limits::default(); limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES); reader.limits(limits); match reader.decode() { Ok(img) => { let rgba = img.into_rgba8(); let orig_width = rgba.width(); let orig_height = rgba.height(); // Resize if target dimensions provided let (final_img, width, height) = if let Some((target_w, target_h)) = target_dimensions { log::info!( "Resizing {}x{} -> {}x{} for memory optimization: {}", orig_width, orig_height, target_w, target_h, path.display() ); // Use Lanczos3 for high-quality downsampling let resized = image::imageops::resize( &rgba, target_w, target_h, image::imageops::FilterType::Lanczos3, ); let resized_w = resized.width(); let resized_h = resized.height(); log::info!( "Resize complete: {}x{} image now uses ~{} MB instead of ~{} MB", resized_w, resized_h, (resized_w as u64 * resized_h as u64 * 4) / MB_TO_BYTES, (orig_width as u64 * orig_height as u64 * 4) / MB_TO_BYTES ); (resized, resized_w, resized_h) } else { log::info!( "Decoded {}x{} image at full resolution: {}", orig_width, orig_height, path.display() ); (rgba, orig_width, orig_height) }; let pixels = final_img.into_raw(); Some((path, width, height, pixels)) } Err(e) => { log::warn!("Failed to decode {}: {}", path.display(), e); None } } } Err(e) => { log::warn!("Failed to guess format for {}: {}", path.display(), e); None } } } Err(e) => { log::warn!("Failed to open {}: {}", path.display(), e); None } } }) .await .ok() .flatten() } /// Manages state and operations for large image decoding in gallery mode #[derive(Debug, Default)] pub struct LargeImageManager { /// Paths of images currently being decoded decoding_images: HashSet, /// Cache of decoded image handles decoded_images: HashMap, /// Display dimensions used for each decoded image (for resize detection) decoded_display_sizes: HashMap, /// Errors encountered during decoding decode_errors: HashMap, /// Generation counter for each decode to support cancellation. /// When a new decode is started for the same path, the generation is incremented. /// Only decodes matching the current generation are accepted when they complete. decode_generations: HashMap, } impl LargeImageManager { pub fn new() -> Self { Self::default() } pub fn is_decoding(&self, path: &Path) -> bool { self.decoding_images.contains(path) } pub fn get_decoded(&self, path: &Path) -> Option<&widget::image::Handle> { self.decoded_images.get(path) } pub fn get_error(&self, path: &Path) -> Option<&String> { self.decode_errors.get(path) } /// Store a decoded image if the generation matches (not superseded by newer decode). /// Returns true if stored, false if rejected due to generation mismatch. pub fn store_decoded_with_generation( &mut self, path: PathBuf, handle: widget::image::Handle, display_size: Option<(u32, u32)>, generation: u64, ) -> bool { // Check if this decode is still current (not superseded by a newer one) if let Some(¤t_gen) = self.decode_generations.get(&path) && generation != current_gen { log::info!( "Discarding outdated decode for {} (generation {} != current {})", path.display(), generation, current_gen ); return false; } log::info!( "Storing decoded image for {} (generation {})", path.display(), generation ); self.decoded_images.insert(path.clone(), handle); if let Some(size) = display_size { self.decoded_display_sizes.insert(path.clone(), size); } self.decoding_images.remove(&path); true } pub fn store_error(&mut self, path: PathBuf, error: String) { self.decode_errors.insert(path.clone(), error); self.decoding_images.remove(&path); } pub fn clear_error(&mut self, path: &Path) { self.decode_errors.remove(path); } pub fn clear_cache(&mut self) { log::info!( "Clearing {} cached images from large image manager", self.decoded_images.len() ); self.decoded_images.clear(); } pub fn cache_size(&self) -> usize { self.decoded_images.len() } pub fn cache_is_empty(&self) -> bool { self.decoded_images.is_empty() } /// Check if an image should be re-decoded due to display size increase. /// Returns true only if the display size has INCREASED by more than 20% in either dimension. /// Does NOT re-decode for smaller sizes (GPU can efficiently downscale). pub fn needs_redecode_for_size( &self, path: &Path, new_display_size: Option<(u32, u32)>, ) -> bool { let Some(new_size) = new_display_size else { return false; }; let Some(&old_size) = self.decoded_display_sizes.get(path) else { return false; }; const REDECODE_THRESHOLD: f32 = 0.2; let width_increase = (new_size.0 as f32 / old_size.0 as f32) - 1.0; let height_increase = (new_size.1 as f32 / old_size.1 as f32) - 1.0; let needs_redecode = width_increase > REDECODE_THRESHOLD || height_increase > REDECODE_THRESHOLD; if needs_redecode { log::info!( "Display size increased significantly for {}: {}x{} -> {}x{} (increase: {:.1}% width, {:.1}% height) - re-decoding at higher resolution", path.display(), old_size.0, old_size.1, new_size.0, new_size.1, width_increase * 100.0, height_increase * 100.0 ); } else if width_increase < -REDECODE_THRESHOLD || height_increase < -REDECODE_THRESHOLD { log::debug!( "Display size decreased for {}: {}x{} -> {}x{} (decrease: {:.1}% width, {:.1}% height) - keeping existing higher resolution", path.display(), old_size.0, old_size.1, new_size.0, new_size.1, width_increase * 100.0, height_increase * 100.0 ); } needs_redecode } /// Attempt to decode a large image, checking memory availability first. /// Returns (should_decode, target_dimensions, generation) tuple. pub fn try_decode( &mut self, path: &PathBuf, display_dimensions: Option<(u32, u32)>, ) -> (bool, Option<(u32, u32)>, u64) { self.clear_error(path); let is_currently_decoding = self.is_decoding(path); let needs_redecode = self.needs_redecode_for_size(path, display_dimensions); if is_currently_decoding && !needs_redecode { // Get current generation for the ongoing decode let generation = self.decode_generations.get(path).copied().unwrap_or(0); return (false, None, generation); } if self.get_decoded(path).is_some() && !needs_redecode && !is_currently_decoding { let generation = self.decode_generations.get(path).copied().unwrap_or(0); return (false, None, generation); } let Some((width, height)) = get_image_dimensions(path) else { self.store_error(path.clone(), "Failed to read image dimensions".to_string()); return (false, None, 0); }; let target_dimensions = if let Some((display_w, display_h)) = display_dimensions { calculate_target_dimensions(width, height, display_w, display_h) } else { None }; // Check memory for target size (if resizing) or full size let (check_w, check_h) = target_dimensions.unwrap_or((width, height)); if !self.ensure_memory_available(path, check_w, check_h) { return (false, None, 0); } // Increment generation counter (cancels any previous decode) let generation = self .decode_generations .entry(path.clone()) .and_modify(|g| *g += 1) .or_insert(1); let generation = *generation; if is_currently_decoding { log::info!( "Cancelling previous decode for {} and starting new one (generation {})", path.display(), generation ); } // Mark as decoding self.decoding_images.insert(path.clone()); (true, target_dimensions, generation) } /// Check if sufficient memory is available, clearing cache if needed. /// Returns true if memory is available, false otherwise. fn ensure_memory_available(&mut self, path: &Path, width: u32, height: u32) -> bool { let (has_memory, error_opt) = check_memory_available(width, height); if has_memory { return true; } if self.cache_is_empty() { if let Some(error_msg) = error_opt { self.store_error(path.to_path_buf(), error_msg); log::warn!( "Cannot load {}: insufficient memory and cache is empty", path.display() ); } return false; } log::info!( "Insufficient memory, clearing {} cached images", self.cache_size() ); self.clear_cache(); let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height); if has_memory_after_clear { log::info!("Memory available after cache clear, proceeding with decode"); return true; } if let Some(error_msg) = error_opt_after { self.store_error(path.to_path_buf(), error_msg); log::warn!( "Cannot load {}: insufficient memory even after cache clear", path.display() ); } false } } ================================================ FILE: src/lib.rs ================================================ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only 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; pub mod app; mod archive; pub mod channel; pub mod clipboard; pub mod config; mod context_action; pub mod dialog; mod key_bind; pub(crate) mod large_image; pub(crate) mod load_image; mod localize; mod menu; mod mime_app; pub mod mime_icon; mod mounter; mod mouse_area; pub mod operation; mod spawn_detached; pub mod tab; mod thumbnail_cacher; mod thumbnailer; pub(crate) mod trash; mod zoom; pub(crate) type FxOrderMap = ordermap::OrderMap; pub(crate) fn err_str(err: T) -> String { err.to_string() } pub fn desktop_dir() -> PathBuf { if let Some(path) = dirs::desktop_dir() { path } else { let path = home_dir().join("Desktop"); log::warn!( "failed to locate desktop directory, falling back to {}", path.display() ); path } } pub fn home_dir() -> PathBuf { if let Some(home) = dirs::home_dir() { home } else { let path = PathBuf::from("/"); log::warn!( "failed to locate home directory, falling back to {}", path.display() ); path } } pub fn is_wayland() -> bool { matches!( cosmic::app::cosmic::windowing_system(), Some(cosmic::app::cosmic::WindowingSystem::Wayland) ) } /// Runs application in desktop mode #[rustfmt::skip] pub fn desktop() -> Result<(), Box> { 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(); let (config_handler, config) = Config::load(); let (state_handler, state) = State::load(); let mut settings = Settings::default(); settings = settings.theme(config.app_theme.theme()); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); settings = settings.exit_on_close(false); settings = settings.transparent(true); #[cfg(all(feature = "wayland", feature = "desktop-applet"))] { settings = settings.no_main_window(true); } let locations = vec![tab::Location::Desktop(desktop_dir(), String::new(), config.desktop)]; let flags = Flags { config_handler, config, state_handler, state, mode: app::Mode::Desktop, locations, uris: Vec::new() }; cosmic::app::run::(settings, flags)?; Ok(()) } /// Runs application with these settings #[rustfmt::skip] pub fn main() -> Result<(), Box> { 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(); let (config_handler, config) = Config::load(); let (state_handler, state) = State::load(); let mut daemonize = true; let mut locations = Vec::new(); let mut uris = Vec::new(); for arg in env::args().skip(1) { let location = if &arg == "--no-daemon" { daemonize = false; continue; } else if &arg == "--trash" { Location::Trash } else if &arg == "--recents" { if config.show_recents { Location::Recents } else { log::warn!("recents feature is disabled in config"); continue; } } else if &arg == "--network" { Location::Network("network:///".to_string(), fl!("networks"), None) } else { //TODO: support more URLs let path = match url::Url::parse(&arg) { Ok(url) if url.scheme() == "file" => if let Ok(path) = url.to_file_path() { path } else { log::warn!("invalid argument {arg:?}"); continue; }, Ok(url) => { uris.push(url); continue; } _ => PathBuf::from(arg), }; match fs::canonicalize(&path) { Ok(absolute) => Location::Path(absolute), Err(err) => { log::warn!("failed to canonicalize {}: {}", path.display(), err); continue; } } }; locations.push(location); } if daemonize { #[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), Err(err) => { eprintln!("failed to daemonize: {err:?}"); process::exit(1); } } } let mut settings = Settings::default(); settings = settings.theme(config.app_theme.theme()); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); settings = settings.exit_on_close(false); #[cfg(feature = "jemalloc")] { settings = settings.default_mmap_threshold(None); } let flags = Flags { config_handler, config, state_handler, state, mode: app::Mode::App, locations, uris }; cosmic::app::run::(settings, flags)?; Ok(()) } ================================================ FILE: src/load_image.rs ================================================ use cosmic::iced::{core as iced_core, widget as iced_widget}; use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, renderer, }; pub fn loaded_image<'a, Message: 'static, Theme>( handle: ::Handle, ) -> LoadedImage<'a, Message, Theme, cosmic::Renderer> where Theme: iced_widget::container::Catalog, ::Class<'a>: From>, { LoadedImage::new(handle) } /// Forces the wrapped image to be loaded before drawing. /// /// May cause a dropped frame if the image is not already in the cache. /// This is useful when you want to ensure an image is loaded before it is drawn, for example when swapping out a placeholder. /// Otherwise, the image may be blank until the next redraw. #[allow(missing_debug_implementations)] pub struct LoadedImage<'a, Message, Theme, Renderer> where Renderer: iced_core::Renderer + iced_core::image::Renderer, { handle: ::Handle, content: cosmic::iced::Element<'a, Message, Theme, Renderer>, } impl<'a, Message, Theme, Renderer> LoadedImage<'a, Message, Theme, Renderer> where Renderer: iced_core::Renderer + iced_core::image::Renderer, ::Handle: 'a, { /// Creates an empty [`LoadedImage`]. pub(crate) fn new(handle: ::Handle) -> Self { LoadedImage { handle: handle.clone(), content: cosmic::widget::Image::new(handle).into(), } } } impl Widget for LoadedImage<'_, Message, Theme, Renderer> where Renderer: iced_core::Renderer + iced_core::image::Renderer, { fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&mut self, tree: &mut Tree) { tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let node = self .content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { operation.container(None, layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() .next() .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, operation, ); }); } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { self.content.as_widget_mut().update( &mut tree.children[0], event, layout .children() .next() .unwrap() .with_virtual_offset(layout.virtual_offset()), cursor_position, renderer, clipboard, shell, viewport, ); } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { let content_layout = layout.children().next().unwrap(); self.content.as_widget().mouse_interaction( &tree.children[0], content_layout.with_virtual_offset(layout.virtual_offset()), cursor_position, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let content_layout = layout.children().next().unwrap(); // forces image to be loaded before drawing _ = renderer.load_image(&self.handle); self.content.as_widget().draw( &tree.children[0], renderer, theme, renderer_style, content_layout.with_virtual_offset(layout.virtual_offset()), cursor_position, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout .children() .next() .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, viewport, translation, ) } fn drag_destinations( &self, state: &Tree, layout: Layout<'_>, renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { let content_layout = layout.children().next().unwrap(); self.content.as_widget().drag_destinations( &state.children[0], content_layout.with_virtual_offset(layout.virtual_offset()), renderer, dnd_rectangles, ); } } impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer + iced_core::image::Renderer, Theme: 'a, { fn from(c: LoadedImage<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { Element::new(c) } } ================================================ FILE: src/localize.rs ================================================ // SPDX-License-Identifier: GPL-3.0-only 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; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { let loader: FluentLanguageLoader = fluent_language_loader!(); loader .load_fallback_language(&Localizations) .expect("Error while loading fallback language"); loader }); pub static LANGUAGE_SORTER: LazyLock = LazyLock::new(|| { let create_collator = |locale: Locale| { let mut prefs = CollatorPreferences::from(locale); prefs.numeric_ordering = Some(CollationNumericOrdering::True); Collator::try_new(prefs, CollatorOptions::default()).ok() }; Locale::try_from_str(&LANGUAGE_LOADER.current_language().to_string()) .ok() .and_then(create_collator) .or_else(|| { Locale::try_from_str(&LANGUAGE_LOADER.fallback_language().to_string()) .ok() .and_then(create_collator) }) .unwrap_or_else(|| { let locale = Locale::try_from_str("en-US").expect("en-US is a valid BCP-47 tag"); create_collator(locale) .expect("Creating a collator from the system's current language, the fallback language, or American English should succeed") }) }); pub static LOCALE: LazyLock = LazyLock::new(|| { for var in ["LC_TIME", "LC_ALL", "LANG"] { if let Ok(locale_str) = std::env::var(var) { let cleaned_locale = locale_str .split('.') .next() .unwrap_or(&locale_str) .replace('_', "-"); if let Ok(locale) = Locale::try_from_str(&cleaned_locale) { return locale; } // Try language-only fallback (e.g., "en" from "en-US") if let Some(lang) = cleaned_locale.split('-').next() && let Ok(locale) = Locale::try_from_str(lang) { return locale; } } } log::warn!("No valid locale found in environment, using fallback"); Locale::try_from_str("en-US").expect("Failed to parse fallback locale 'en-US'") }); #[macro_export] macro_rules! fl { ($message_id:literal) => {{ i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) }}; ($message_id:literal, $($args:expr),*) => {{ i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) }}; } // Get the `Localizer` to be used for localizing this library. pub fn localizer() -> Box { Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) } pub fn localize() { let localizer = localizer(); let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); if let Err(error) = localizer.select(&requested_languages) { eprintln!("Error while loading language for COSMIC Files {error}"); } } ================================================ FILE: src/main.rs ================================================ #[cfg(feature = "jemalloc")] use tikv_jemallocator::Jemalloc; #[cfg(feature = "jemalloc")] #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; fn main() -> Result<(), Box> { let _ = jxl_oxide::integration::register_image_decoding_hook(); cosmic_files::main() } ================================================ FILE: src/menu.rs ================================================ // SPDX-License-Identifier: GPL-3.0-only 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}; use i18n_embed::LanguageLoader; use mime_guess::Mime; use std::collections::HashMap; use std::sync::LazyLock; 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 = LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); macro_rules! menu_button { ($($x:expr),+ $(,)?) => ( button::custom( Row::with_children( [$(Element::from($x)),+] ) .height(Length::Fixed(24.0)) .align_y(Alignment::Center) ) .padding([theme::spacing().space_xxs, 16]) .width(Length::Fill) .class(theme::Button::MenuItem) ); } const fn menu_button_optional( label: String, action: Action, enabled: bool, ) -> menu::Item { if enabled { menu::Item::Button(label, None, action) } else { menu::Item::ButtonDisabled(label, None, action) } } pub fn context_menu<'a>( tab: &Tab, key_binds: &HashMap, 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 { if action == key_action { return key_bind.to_string(); } } String::new() }; fn key_style(theme: &cosmic::Theme) -> TextStyle { let mut color = theme.cosmic().background.component.on; color.alpha *= 0.75; TextStyle { color: Some(color.into()), } } fn disabled_style(theme: &cosmic::Theme) -> TextStyle { let mut color = theme.cosmic().background.component.on; color.alpha *= 0.5; TextStyle { color: Some(color.into()), } } let menu_item = |label, action| { let key = find_key(&action); menu_button!( text::body(label), space::horizontal(), text::body(key).class(theme::Text::Custom(key_style)) ) .on_press(tab::Message::ContextAction(action)) }; let menu_item_disabled = |label, action: Action| { let key = find_key(&action); menu_button!( text::body(label).class(theme::Text::Custom(disabled_style)), space::horizontal(), text::body(key).class(theme::Text::Custom(disabled_style)) ) }; // Allow paste when clipboard has data and we're in a location that supports it let can_paste = clipboard_paste_available && tab.location.supports_paste(); let (sort_name, sort_direction, _) = tab.sort_options(); let sort_item = |label, variant| { menu_item( format!( "{} {}", label, match (sort_name == variant, sort_direction) { (true, true) => "\u{2B07}", (true, false) => "\u{2B06}", _ => "", } ), Action::ToggleSort(variant), ) .into() }; let mut selected_dir = 0; let mut selected = 0; let mut selected_trash_only = false; let mut selected_desktop_entry = None; let mut selected_types: Vec = vec![]; let mut selected_mount_point = 0; if let Some(items) = tab.items_opt() { for item in items { if item.selected { selected += 1; if item.metadata.is_dir() { selected_mount_point += i32::from(item.is_mount_point); selected_dir += 1; } match &item.location_opt { Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => { selected_trash_only = true } Some(Location::Path(path)) if selected == 1 && path.extension().and_then(|s| s.to_str()) == Some("desktop") => { selected_desktop_entry = Some(&**path); } _ => (), } selected_types.push(item.mime.clone()); } } } 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::>>() }; // Parse the desktop entry if it is the only selection #[cfg(feature = "desktop")] let selected_desktop_entry = selected_desktop_entry.and_then(|path| { if selected == 1 { let lang_id = crate::localize::LANGUAGE_LOADER.current_language(); let language = lang_id.language.as_str(); // Cache? cosmic::desktop::load_desktop_file(&[language.into()], path.into()) } else { None } }); let mut children: Vec> = Vec::new(); match (&tab.mode, &tab.location) { ( tab::Mode::App | tab::Mode::Desktop, Location::Desktop(..) | Location::Path(..) | Location::Search(SearchLocation::Path(..), ..) | Location::Search(SearchLocation::Recents, ..) | Location::Recents | Location::Network(_, _, Some(_)), ) => { if selected_trash_only { children.push(menu_item(fl!("open"), Action::Open).into()); if !Trash::is_empty() { children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); } } 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( |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(), )); } children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("cut"), Action::Cut).into()); if modifiers.shift() && !modifiers.control() { children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); } else { children.push(menu_item(fl!("copy"), Action::Copy).into()); } // 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()); } if selected == 1 { children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into()); if selected_dir == 1 { children .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); } } if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) { children.push( menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), ); } // All selected items are directories if selected == selected_dir && matches!(tab.mode, tab::Mode::App) { children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into()); 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()); children.push(menu_item(fl!("cut"), Action::Cut).into()); } if modifiers.shift() && !modifiers.control() { children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); } else { children.push(menu_item(fl!("copy"), Action::Copy).into()); } if selected_mount_point == 0 { children.push(menu_item(fl!("move-to"), Action::MoveTo).into()); } children.push(menu_item(fl!("copy-to"), Action::CopyTo).into()); children.push(divider::horizontal::light().into()); let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m)); if selected_types.is_empty() { children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into()); children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into()); } children.push(menu_item(fl!("compress"), Action::Compress).into()); children.push(divider::horizontal::light().into()); //TODO: Print? children.push(menu_item(fl!("show-details"), Action::Preview).into()); if matches!(tab.mode, tab::Mode::App) { children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); } children.push(divider::horizontal::light().into()); if tab.location.is_recents() { children.push( menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(), ); children.push(divider::horizontal::light().into()); } if selected_mount_point == 0 { if modifiers.shift() && !modifiers.control() { children.push( menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(), ); } else { children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); } } else if selected == 1 { children.push(menu_item(fl!("eject"), Action::Eject).into()); } } else { //TODO: need better designs for menu with no selection //TODO: have things like properties but they apply to the folder? if tab.location != Location::Recents { children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); children.push(menu_item(fl!("new-file"), Action::NewFile).into()); children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); children.push(divider::horizontal::light().into()); } if tab.mode.multiple() { children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); } if can_paste { children.push(menu_item(fl!("paste"), Action::Paste).into()); } else { children.push(menu_item_disabled(fl!("paste"), Action::Paste).into()); } //TODO: only show if cosmic-settings is found? if matches!(tab.mode, tab::Mode::Desktop) { children.push(divider::horizontal::light().into()); children.push( menu_item(fl!("change-wallpaper"), Action::CosmicSettingsWallpaper).into(), ); children.push( menu_item(fl!("desktop-appearance"), Action::CosmicSettingsDesktop).into(), ); children.push( menu_item(fl!("display-settings"), Action::CosmicSettingsDisplays).into(), ); } children.push(divider::horizontal::light().into()); // TODO: Nested menu children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); if matches!(tab.location, Location::Desktop(..)) { children.push(divider::horizontal::light().into()); children.push( menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(), ); } } } ( tab::Mode::Dialog(dialog_kind), Location::Desktop(..) | Location::Path(..) | Location::Search(SearchLocation::Path(..), ..) | Location::Search(SearchLocation::Recents, ..) | Location::Recents | Location::Network(_, _, Some(_)), ) => { if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { children.push(menu_item(fl!("open"), Action::Open).into()); } if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() { children.push( menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), ); } children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("show-details"), Action::Preview).into()); } else { if dialog_kind.save() { children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); } if tab.mode.multiple() { children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); } if !children.is_empty() { children.push(divider::horizontal::light().into()); } children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); } } (_, Location::Network(..)) => { if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { children.push(menu_item(fl!("open"), Action::Open).into()); } } else { if tab.mode.multiple() { children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); } if !children.is_empty() { children.push(divider::horizontal::light().into()); } children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); } } (_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => { if tab.mode.multiple() { children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); } if !children.is_empty() { children.push(divider::horizontal::light().into()); } if selected > 0 { children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(divider::horizontal::light().into()); children .push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into()); children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("delete-permanently"), Action::Delete).into()); } else { // TODO: Nested menu children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-trashed"), HeadingOptions::TrashedOn)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); } } } container(column::with_children(children)) .padding(1) //TODO: move style to libcosmic .style(|theme| { let cosmic = theme.cosmic(); let component = &cosmic.background.component; container::Style { icon_color: Some(component.on.into()), text_color: Some(component.on.into()), background: Some(Background::Color(component.base.into())), border: Border { radius: cosmic.radius_s().map(|x| x + 1.0).into(), width: 1.0, color: component.divider.into(), }, ..Default::default() } }) .width(Length::Fixed(360.0)) .into() } pub fn dialog_menu( tab: &Tab, key_binds: &HashMap, show_details: bool, ) -> Element<'static, Message> { let (sort_name, sort_direction, _) = tab.sort_options(); let sort_item = |label, sort, dir| { menu::Item::CheckBox( label, None, sort_name == sort && sort_direction == dir, Action::SetSort(sort, dir), ) }; let in_trash = tab.location.is_trash(); let mut selected_gallery = 0; if let Some(items) = tab.items_opt() { for item in items { if item.selected && item.can_gallery() { selected_gallery += 1; } } } MenuBar::new(vec![ menu::Tree::with_children( Element::from( widget::button::icon(widget::icon::from_name(match tab.config.view { tab::View::Grid => "view-grid-symbolic", tab::View::List => "view-list-symbolic", })) // This prevents the button from being shown as insensitive .on_press(Message::None) .padding(8), ), menu::items( key_binds, vec![ menu::Item::CheckBox( fl!("grid-view"), None, matches!(tab.config.view, tab::View::Grid), Action::TabViewGrid, ), menu::Item::CheckBox( fl!("list-view"), None, matches!(tab.config.view, tab::View::List), Action::TabViewList, ), ], ), ), menu::Tree::with_children( Element::from( widget::button::icon(widget::icon::from_name(if sort_direction { "view-sort-ascending-symbolic" } else { "view-sort-descending-symbolic" })) // This prevents the button from being shown as insensitive .on_press(Message::None) .padding(8), ), menu::items( key_binds, vec![ sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true), sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false), sort_item( fl!("sort-newest-first"), if in_trash { tab::HeadingOptions::TrashedOn } else { tab::HeadingOptions::Modified }, false, ), sort_item( fl!("sort-oldest-first"), if in_trash { tab::HeadingOptions::TrashedOn } else { tab::HeadingOptions::Modified }, true, ), sort_item( fl!("sort-smallest-to-largest"), tab::HeadingOptions::Size, true, ), sort_item( fl!("sort-largest-to-smallest"), tab::HeadingOptions::Size, false, ), //TODO: sort by type ], ), ), menu::Tree::with_children( Element::from( widget::button::icon(widget::icon::from_name("view-more-symbolic")) // This prevents the button from being shown as insensitive .on_press(Message::None) .padding(8), ), menu::items( key_binds, vec![ menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn), menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault), menu::Item::Button(fl!("zoom-out"), None, Action::ZoomOut), menu::Item::Divider, menu::Item::CheckBox( fl!("show-hidden-files"), None, tab.config.show_hidden, Action::ToggleShowHidden, ), menu::Item::CheckBox( fl!("list-directories-first"), None, tab.config.folders_first, Action::ToggleFoldersFirst, ), menu::Item::CheckBox(fl!("show-details"), None, show_details, Action::Preview), menu::Item::Divider, menu_button_optional( fl!("gallery-preview"), Action::Gallery, selected_gallery > 0, ), ], ), ), ]) .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) .spacing(theme::spacing().space_xxxs.into()) .into() } pub fn menu_bar<'a>( core: &Core, tab_opt: Option<&Tab>, config: &Config, modifiers: &Modifiers, key_binds: &HashMap, clipboard_paste_available: bool, ) -> Element<'a, Message> { let sort_options = tab_opt.map(Tab::sort_options); let sort_item = |label, sort, dir| { menu::Item::CheckBox( label, None, sort_options.is_some_and(|(sort_name, sort_direction, _)| { sort_name == sort && sort_direction == dir }), Action::SetSort(sort, dir), ) }; let in_trash = tab_opt.is_some_and(|tab| tab.location.is_trash()); let mut selected_dir = 0; let mut selected = 0; let mut selected_gallery = 0; if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) { for item in items { if item.selected { selected += 1; if item.metadata.is_dir() { selected_dir += 1; } if item.can_gallery() { selected_gallery += 1; } } } } // Allow paste when clipboard has data and we're in a location that supports it let can_paste = clipboard_paste_available && tab_opt.is_some_and(|tab| tab.location.supports_paste()); let (delete_item, delete_item_action) = if in_trash || modifiers.shift() { (fl!("delete-permanently"), Action::Delete) } else { (fl!("move-to-trash"), Action::Delete) }; responsive_menu_bar() .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) .spacing(theme::spacing().space_xxxs.into()) .into_element( core, key_binds, MENU_ID.clone(), Message::Surface, vec![ ( fl!("file"), vec![ menu::Item::Button(fl!("new-tab"), None, Action::TabNew), menu::Item::Button(fl!("new-window"), None, Action::WindowNew), menu::Item::Button(fl!("new-folder"), None, Action::NewFolder), menu::Item::Button(fl!("new-file"), None, Action::NewFile), menu_button_optional( fl!("open"), Action::Open, (selected > 0 && selected_dir == 0) || (selected_dir == 1 && selected == 1), ), menu_button_optional( fl!("menu-open-with"), Action::OpenWith, selected == 1, ), menu::Item::Divider, menu_button_optional(fl!("rename"), Action::Rename, selected > 0), menu::Item::Divider, menu::Item::Button(fl!("reload-folder"), None, Action::Reload), menu::Item::Divider, menu_button_optional( fl!("add-to-sidebar"), Action::AddToSidebar, selected > 0, ), menu::Item::Divider, menu_button_optional( fl!("restore-from-trash"), Action::RestoreFromTrash, selected > 0 && in_trash, ), menu_button_optional(delete_item, delete_item_action, selected > 0), menu::Item::Divider, menu::Item::Button(fl!("close-tab"), None, Action::TabClose), menu::Item::Button(fl!("quit"), None, Action::WindowClose), ], ), ( (fl!("edit")), vec![ menu_button_optional(fl!("cut"), Action::Cut, selected > 0), menu_button_optional(fl!("copy"), Action::Copy, selected > 0), menu_button_optional(fl!("move-to"), Action::MoveTo, selected > 0), menu_button_optional(fl!("copy-to"), Action::CopyTo, selected > 0), menu_button_optional(fl!("paste"), Action::Paste, can_paste), menu::Item::Button(fl!("select-all"), None, Action::SelectAll), menu::Item::Divider, menu::Item::Button(fl!("history"), None, Action::EditHistory), ], ), ( (fl!("view")), vec![ menu::Item::Button(fl!("zoom-in"), None, Action::ZoomIn), menu::Item::Button(fl!("default-size"), None, Action::ZoomDefault), menu::Item::Button(fl!("zoom-out"), None, Action::ZoomOut), menu::Item::Divider, menu::Item::CheckBox( fl!("grid-view"), None, tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::Grid)), Action::TabViewGrid, ), menu::Item::CheckBox( fl!("list-view"), None, tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::List)), Action::TabViewList, ), menu::Item::Divider, menu::Item::CheckBox( fl!("show-hidden-files"), None, tab_opt.is_some_and(|tab| tab.config.show_hidden), Action::ToggleShowHidden, ), menu::Item::CheckBox( fl!("list-directories-first"), None, tab_opt.is_some_and(|tab| tab.config.folders_first), Action::ToggleFoldersFirst, ), menu::Item::CheckBox( fl!("show-details"), None, config.show_details, Action::Preview, ), menu::Item::Divider, menu_button_optional( fl!("gallery-preview"), Action::Gallery, selected_gallery > 0, ), menu::Item::Divider, menu::Item::Button(fl!("menu-settings"), None, Action::Settings), menu::Item::Divider, menu::Item::Button(fl!("menu-about"), None, Action::About), ], ), ( (fl!("sort")), vec![ sort_item(fl!("sort-a-z"), tab::HeadingOptions::Name, true), sort_item(fl!("sort-z-a"), tab::HeadingOptions::Name, false), sort_item( fl!("sort-newest-first"), if in_trash { tab::HeadingOptions::TrashedOn } else { tab::HeadingOptions::Modified }, false, ), sort_item( fl!("sort-oldest-first"), if in_trash { tab::HeadingOptions::TrashedOn } else { tab::HeadingOptions::Modified }, true, ), sort_item( fl!("sort-smallest-to-largest"), tab::HeadingOptions::Size, true, ), sort_item( fl!("sort-largest-to-smallest"), tab::HeadingOptions::Size, false, ), //TODO: sort by type ], ), ], ) } pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> { //TODO: only add some of these when in App mode let children = [ menu_button!(text::body(fl!("open-in-new-tab"))) .on_press(tab::Message::LocationMenuAction( LocationMenuAction::OpenInNewTab(ancestor_index), )) .into(), menu_button!(text::body(fl!("open-in-new-window"))) .on_press(tab::Message::LocationMenuAction( LocationMenuAction::OpenInNewWindow(ancestor_index), )) .into(), divider::horizontal::light().into(), menu_button!(text::body(fl!("show-details"))) .on_press(tab::Message::LocationMenuAction( LocationMenuAction::Preview(ancestor_index), )) .into(), divider::horizontal::light().into(), menu_button!(text::body(fl!("add-to-sidebar"))) .on_press(tab::Message::LocationMenuAction( LocationMenuAction::AddToSidebar(ancestor_index), )) .into(), ]; container(column::with_children(children)) .padding(1) .style(|theme| { let cosmic = theme.cosmic(); let component = &cosmic.background.component; container::Style { icon_color: Some(component.on.into()), text_color: Some(component.on.into()), background: Some(Background::Color(component.base.into())), border: Border { radius: cosmic.radius_s().map(|x| x + 1.0).into(), width: 1.0, color: component.divider.into(), }, ..Default::default() } }) .width(Length::Fixed(360.0)) .into() } ================================================ FILE: src/mime_app.rs ================================================ // Copyright 2023 System76 // 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; use std::cmp::Ordering; use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::time::Instant; use std::{fs, io, process}; pub fn exec_to_command( exec: &str, entry_name: &str, entry_path: Option<&Path>, path_opt: &[impl AsRef], ) -> Option> { let arguments = shlex::split(exec)?; if arguments.is_empty() { tracing::error!("command does not contain any arguments"); return None; } let mut commands = Vec::new(); let paths = path_opt .iter() .map(AsRef::as_ref) .map(Some) // Add a single `None` if no path was given. .chain(std::iter::repeat_n( None, if path_opt.is_empty() { 1 } else { 0 }, )); for path in paths { let mut batch_process = false; let mut args = Vec::with_capacity(arguments.len()); let mut field_code_used = false; for argument in arguments.iter().skip(1) { let mut new_argument = BString::new(Vec::with_capacity(argument.capacity())); let mut chars = argument.chars(); while let Some(char) = chars.next() { // https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html if char == '%' { match chars.next() { Some('%') => new_argument.push_char(char), Some('c') => new_argument.push_str(entry_name), Some('k') => { if let Some(path) = entry_path { new_argument.push_str(path.as_os_str().as_bytes()); } } // %f and %u behave the same in a file manager. Some('f' | 'u') => { if let Some(path) = path && !field_code_used { // TODO: files on remote file systems should be copied to a temporary local file. batch_process = true; field_code_used = true; new_argument.push_str(path.as_bytes()); } } // %F and %U behave the same in a file manager. Some('F') | Some('U') => { if !field_code_used && new_argument.is_empty() { field_code_used = true; for path in path_opt.iter().map(AsRef::as_ref) { args.push(BString::new(path.as_bytes().to_owned())); } } } _ => (), } } else { new_argument.push_char(char); } } if !new_argument.is_empty() { args.push(new_argument); } } 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; } } } commands.push(command); if !batch_process { break; } } #[cfg(debug_assertions)] for command in &commands { log::debug!( "Parsed program {} with args: {:?}", command.get_program().to_string_lossy(), command.get_args() ); } Some(commands) } #[derive(Clone, Debug)] pub struct MimeApp { pub id: String, pub path: Option, pub name: String, pub exec: Option, pub icon: widget::icon::Handle, pub is_default: bool, } impl MimeApp { //TODO: move to libcosmic, support multiple files pub fn command>(&self, path_opt: &[O]) -> Option> { exec_to_command( self.exec.as_deref()?, &self.name, self.path.as_deref(), path_opt, ) } } // This allows usage of MimeApp in a dropdown impl AsRef for MimeApp { fn as_ref(&self) -> &str { &self.name } } #[cfg(feature = "desktop")] impl From<&desktop::DesktopEntryData> for MimeApp { fn from(app: &desktop::DesktopEntryData) -> Self { Self { id: app.id.clone(), path: app.path.clone(), name: app.name.clone(), exec: app.exec.clone(), icon: match &app.icon { desktop::fde::IconSource::Name(name) => { widget::icon::from_name(name.as_str()).size(32).handle() } desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()), }, is_default: false, } } } #[cfg(feature = "desktop")] fn filename_eq(path_opt: &Option, filename: &str) -> bool { path_opt .as_ref() .and_then(|path| path.file_name()) .is_some_and(|x| x == filename) } pub struct MimeAppCache { apps: Vec, cache: FxHashMap>, icons: FxHashMap>, terminals: Vec, } impl MimeAppCache { pub fn new() -> Self { let mut mime_app_cache = Self { apps: Vec::new(), cache: FxHashMap::default(), icons: FxHashMap::default(), terminals: Vec::new(), }; mime_app_cache.reload(); mime_app_cache } #[cfg(not(feature = "desktop"))] pub fn reload(&mut self) {} // Only available when using desktop feature of libcosmic, which only works on Unix-likes #[cfg(feature = "desktop")] pub fn reload(&mut self) { use crate::localize::LANGUAGE_SORTER; let start = Instant::now(); self.apps.clear(); self.cache.clear(); self.icons.clear(); self.terminals.clear(); //TODO: get proper locale? let locale = &[]; // Load desktop applications by supported mime types //TODO: hashmap for all apps by id? let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect(); for app in &all_apps { //TODO: just collect apps that can be executed with a file argument? if !app.mime_types.is_empty() { self.apps.push(MimeApp::from(app)); } for mime in &app.mime_types { let apps = self .cache .entry(mime.clone()) .or_insert_with(|| Vec::with_capacity(1)); if !apps.iter().any(|x| x.id == app.id) { apps.push(MimeApp::from(app)); } } for category in &app.categories { if category == "TerminalEmulator" { self.terminals.push(MimeApp::from(app)); break; } } } let mut list = cosmic_mime_apps::List::default(); let paths = cosmic_mime_apps::list_paths(); list.load_from_paths(&paths); for (mime, filenames) in list .added_associations .iter() .chain(list.default_apps.iter()) { for filename in filenames { log::trace!("add {mime}={filename}"); let apps = self .cache .entry(mime.clone()) .or_insert_with(|| Vec::with_capacity(1)); if !apps.iter().any(|x| filename_eq(&x.path, filename)) { if let Some(app) = all_apps.iter().find(|&x| filename_eq(&x.path, filename)) { apps.push(MimeApp::from(app)); } else { log::info!( "failed to add association for {mime:?}: application {filename:?} not found" ); } } } } for (mime, filenames) in list.removed_associations.iter() { for filename in filenames { log::trace!("remove {mime}={filename}"); if let Some(apps) = self.cache.get_mut(mime) { apps.retain(|x| !filename_eq(&x.path, filename)); } } } for (mime, filenames) in list.default_apps.iter() { for filename in filenames { log::trace!("default {mime}={filename}"); if let Some(apps) = self.cache.get_mut(mime) { let mut found = false; for app in apps.iter_mut() { if filename_eq(&app.path, filename) { app.is_default = true; found = true; } else { app.is_default = false; } } if found { break; } log::debug!( "failed to set default for {mime:?}: application {filename:?} not found" ); } } } // Sort apps by name self.apps .sort_by(|a, b| match (a.is_default, b.is_default) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => LANGUAGE_SORTER.compare(&a.name, &b.name), }); for apps in self.cache.values_mut() { apps.sort_by(|a, b| match (a.is_default, b.is_default) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => LANGUAGE_SORTER.compare(&a.name, &b.name), }); } // Copy icons to special cache //TODO: adjust dropdown API so this is no longer needed self.icons.extend(self.cache.iter().map(|(mime, apps)| { ( mime.clone(), apps.iter().map(|app| app.icon.clone()).collect(), ) })); let elapsed = start.elapsed(); log::info!("loaded mime app cache in {elapsed:?}"); } pub fn apps(&self) -> &[MimeApp] { &self.apps } pub fn get(&self, key: &Mime) -> &[MimeApp] { self.cache.get(key).map_or(&[], Vec::as_slice) } pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] { self.icons.get(key).map_or(&[], Box::as_ref) } fn get_default_terminal(&self) -> Option { let output = process::Command::new("xdg-mime") .args(["query", "default", "x-scheme-handler/terminal"]) .output() .ok()?; if !output.status.success() { return None; } String::from_utf8(output.stdout) .ok() .map(|string| string.trim().replace(".desktop", "")) } pub fn terminal(&self) -> Option<&MimeApp> { //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec // The current approach works but might not adhere to the spec (yet) // Look for and return preferred terminals //TODO: fallback order beyond cosmic-term? let mut preference_order = vec!["com.system76.CosmicTerm".to_string()]; if let Some(id) = self.get_default_terminal() { preference_order.insert(0, id); } for id in &preference_order { for terminal in &self.terminals { if &terminal.id == id { return Some(terminal); } } } // Return whatever was the first terminal found self.terminals.first() } #[cfg(not(feature = "desktop"))] pub fn set_default(&mut self, mime: Mime, id: String) { log::warn!( "failed to set default handler for {mime:?} to {id:?}: desktop feature not enabled" ); } #[cfg(feature = "desktop")] pub fn set_default(&mut self, mime: Mime, mut id: String) { let Some(path) = cosmic_mime_apps::local_list_path() else { log::warn!("failed to find mimeapps.list path"); return; }; let mut list = cosmic_mime_apps::List::default(); match fs::read_to_string(&path) { Ok(string) => { list.load_from(&string); } Err(err) => { if err.kind() != io::ErrorKind::NotFound { log::warn!("failed to read {}: {}", path.display(), err); return; } } } let suffix = ".desktop"; if !id.ends_with(suffix) { id.push_str(suffix); } list.set_default_app(mime, id); let mut string = list.to_string(); string.push('\n'); match fs::write(&path, string) { Ok(()) => { self.reload(); } Err(err) => { log::warn!("failed to write {}: {}", path.display(), err); } } } } impl Default for MimeAppCache { fn default() -> Self { Self::new() } } #[cfg(test)] 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, "one_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!( "file1", command.get_args().next().unwrap().to_str().unwrap() ); } #[test] #[allow(non_snake_case)] fn one_path_F_field_code() { 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/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/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/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] #[allow(non_snake_case)] fn one_path_U_field_code() { let exec = "/usr/bin/rmrfbye %U"; let paths = ["/"]; 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(); assert_eq!("/usr/bin/rmrfbye", command.get_program().to_str().unwrap()); assert_eq!("/", command.get_args().next().unwrap().to_str().unwrap()); } #[test] fn mult_path_f_field_code() { let exec = "/usr/games/ppsspp %f"; let paths = [ "/usr/share/games/psp/miku.iso", "/usr/share/games/psp/eternia.iso", ]; 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()) { assert_eq!("/usr/games/ppsspp", command.get_program().to_str().unwrap()); assert_eq!(1, command.get_args().len()); let command_path = command.get_args().next().unwrap(); assert_eq!(*path, command_path.to_str().unwrap()); } } #[test] #[allow(non_snake_case)] fn mult_path_F_field_code() { let exec = "/usr/games/gzdoom %F"; let paths = [ "/usr/share/games/doom2/hr.wad", "/usr/share/games/doom2/hrmus.wad", ]; 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(); assert_eq!("/usr/games/gzdoom", command.get_program().to_str().unwrap()); assert!( paths .iter() .zip(command.get_args()) .all(|(&expected, actual)| expected == actual.to_string_lossy()) ); } #[test] fn mult_path_u_field_code() { let exec = "/usr/bin/cosmic_browser %u"; let paths = [ "file:///home/josh/Books/osstep.pdf", "https://redox-os.org/", "https://system76.com/", ]; 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()) { assert_eq!( "/usr/bin/cosmic_browser", command.get_program().to_str().unwrap() ); assert_eq!(1, command.get_args().len()); let command_path = command.get_args().next().unwrap(); assert_eq!(*path, command_path.to_str().unwrap()); } } #[test] #[allow(non_snake_case)] fn mult_path_U_field_code() { let exec = "/usr/bin/mpv %U"; let paths = [ "frieren01.mkv", "rtmp://example.org/this/video/doesnt/exist.avi", ]; 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(); assert_eq!(paths.len(), command.get_args().count()); assert_eq!("/usr/bin/mpv", command.get_program().to_str().unwrap()); assert!( paths .iter() .zip(command.get_args()) .all(|(&expected, actual)| expected == actual.to_string_lossy()) ); } #[test] fn flatpak_style_exec() { // Tests args before field codes let exec = "/usr/bin/flatpak run --branch=stable --command=ferris --file-forwarding org.joshfake.ferris @@u %U"; let args = [ "run", "--branch=stable", "--command=ferris", "--file-forwarding", "org.joshfake.ferris", "@@u", ]; let paths = ["file1.rs", "file2.rs"]; 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(); assert_eq!(args.len() + paths.len(), command.get_args().count()); assert_eq!("/usr/bin/flatpak", command.get_program().to_str().unwrap()); assert!( args.iter() .chain(paths.iter()) .zip(command.get_args()) .all(|(&expected, actual)| expected == actual.to_string_lossy()) ); } #[test] fn multiple_field_codes() { // Tests that only one field code is used rather than passing paths to each field code let exec = "/usr/games/roguelike %U %f"; let paths = [ "file:///usr/share/games/roguelike/mods/mod1", "file:///usr/share/games/roguelike/mods/mod2", ]; 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(); assert_eq!( "/usr/games/roguelike", command.get_program().to_str().unwrap() ); assert!( paths .iter() .zip(command.get_args()) .all(|(&expected, actual)| expected == actual.to_string_lossy()) ); } #[test] fn sandwiched_field_code() { // Tests that arguments before and after the field code works // (Borrowed from KDE because someone had this exact line in an issue) let exec = "/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=okular --file-forwarding org.kde.okular @@u %U @@"; let args_leading = [ "run", "--branch=stable", "--arch=x86_64", "--command=okular", "--file-forwarding", "org.kde.okular", "@@u", ]; let paths = ["rust_game_dev.pdf", "superhero_ferris.epub"]; let args_trailing = ["@@"]; 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(); assert_eq!( args_leading.len() + paths.len() + args_trailing.len(), command.get_args().len() ); assert_eq!("/usr/bin/flatpak", command.get_program().to_str().unwrap()); assert!( args_leading .iter() .chain(paths.iter()) .chain(args_trailing.iter()) .zip(command.get_args()) .all(|(&expected, actual)| expected == actual.to_string_lossy()) ); } } ================================================ FILE: src/mime_icon.rs ================================================ // SPDX-License-Identifier: GPL-3.0-only use cosmic::widget::icon; use mime_guess::Mime; use rustc_hash::FxHashMap; use std::fs; use std::path::Path; use std::sync::{LazyLock, Mutex}; pub const FALLBACK_MIME_ICON: &str = "text-x-generic"; #[derive(Debug, Eq, Hash, PartialEq)] struct MimeIconKey { mime: Mime, size: u16, } struct MimeIconCache { cache: FxHashMap>, #[cfg(unix)] shared_mime_info: xdg_mime::SharedMimeInfo, } impl MimeIconCache { pub fn new() -> Self { Self { cache: FxHashMap::default(), #[cfg(unix)] shared_mime_info: xdg_mime::SharedMimeInfo::new(), } } #[cfg(not(unix))] pub fn get(&mut self, _key: MimeIconKey) -> Option { None } #[cfg(unix)] pub fn get(&mut self, key: MimeIconKey) -> Option { self.cache .entry(key) .or_insert_with_key(|key| { let mut icon_names = self.shared_mime_info.lookup_icon_names(&key.mime); if icon_names.is_empty() { return None; } let icon_name = icon_names.remove(0); let mut named = icon::from_name(icon_name).size(key.size); if !icon_names.is_empty() { let fallback_names = icon_names.into_iter().map(std::borrow::Cow::from).collect(); named = named.fallback(Some(icon::IconFallback::Names(fallback_names))); } Some(named.handle()) }) .clone() } } static MIME_ICON_CACHE: LazyLock> = LazyLock::new(|| Mutex::new(MimeIconCache::new())); #[cfg(not(unix))] pub fn mime_for_path( path: impl AsRef, 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, metadata_opt: Option<&fs::Metadata>, remote: bool, ) -> Mime { let path = path.as_ref(); let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); // Try the shared mime info cache first let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type(); if remote { if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) { gb.file_name(file_name); } } else { gb.path(path); } if let Some(metadata) = metadata_opt { gb.metadata(metadata.clone()); } let guess = gb.guess(); let guessed_mime = guess.mime_type(); /// Checks if the `Mime` is a special variant returned by `xdg-mime`. /// This includes directories, symlinks and zerosize files, which are returned as uncertain. fn is_special_mime(mime: &Mime) -> bool { *mime == "inode/directory" || *mime == "inode/symlink" || *mime == "application/x-zerosize" } // `xdg-mime-rs` sets the guess to uncertain if it returns special mime types. // The guess could also be uncertain on platforms without shared-mime-info. // Try mime_guess, but only if it is not one of the special mime types. if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) { // If uncertain, try mime_guess. This could happen on platforms without shared-mime-info mime_guess::from_path(path).first_or_octet_stream() } else { guessed_mime.clone() } } 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(), } } #[cfg(not(unix))] pub fn parent_mime_types(_mime: &Mime) -> Option> { None } #[cfg(unix)] pub fn parent_mime_types(mime: &Mime) -> Option> { let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); mime_icon_cache.shared_mime_info.get_parents_aliased(mime) } ================================================ FILE: src/mounter/gvfs.rs ================================================ 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; use crate::err_str; use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location}; const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri"; fn resolve_uri(uri: &str) -> (String, gio::File) { let file = gio::File::for_uri(uri); // Resolve the target-uri if it exists if let Ok(file_info) = file.query_info( TARGET_URI_ATTRIBUTE, gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE, ) && let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { let resolved_uri = String::from(resolved_uri); let file = gio::File::for_uri(&resolved_uri); return (resolved_uri, file); } (uri.to_string(), file) } fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { if let Some(themed_icon) = icon.downcast_ref::() { for name in themed_icon.names() { let named = widget::icon::from_name(name.as_str()).size(size); if let Some(path) = named.path() { return Some(path); } } } //TODO: handle more gio icon types None } fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems { let mut items: MounterItems = (monitor.mounts().into_iter()) .enumerate() // Hide shadowed mounts .filter(|(_, mount)| !mount.is_shadowed()) .map(|(i, mount)| { let root = MountExt::root(&mount); let is_remote = root .query_filesystem_info( gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE, gio::Cancellable::NONE, ) .ok() .map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE)) .unwrap_or(true); // Default to remote if query fails MounterItem::Gvfs(Item { uri: mount.root().uri().into(), kind: ItemKind::Mount, index: i, name: mount.name().into(), is_mounted: true, is_remote, icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()), icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16), path_opt: root.path(), }) }) .collect(); items.extend( (monitor.volumes().into_iter()) .enumerate() // Volumes with mounts are already listed by mount .filter(|(_, volume)| volume.get_mount().is_none()) .map(|(i, volume)| { let uri = VolumeExt::activation_root(&volume) .map(|f| f.uri().into()) .unwrap_or_default(); MounterItem::Gvfs(Item { // TODO can we get URI for volumes with no mount? uri, kind: ItemKind::Volume, index: i, name: volume.name().into(), is_mounted: false, is_remote: false, icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()), icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16), path_opt: None, }) }), ); items } fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { let force_dir = uri.starts_with("network:///"); let (_, file) = resolve_uri(uri); // Read .hidden file if present let hidden_files: Box<[String]> = if let Some(path) = file.path() { let hidden_file_path = path.join(".hidden"); if hidden_file_path.is_file() { tab::parse_hidden_file(&hidden_file_path) } else { Box::from([]) } } else { Box::from([]) }; let mut items = Vec::new(); for info_res in file .enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) .map_err(err_str)? { let info = info_res.map_err(err_str)?; let name = info.name().to_string_lossy().into_owned(); let display_name = String::from(info.display_name()); let uri = String::from(file.child(info.name()).uri()); //TODO: what is the best way to resolve shortcuts? let location = Location::Network(uri, display_name.clone(), file.child(&name).path()); let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) { let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let is_dir = matches!(info.file_type(), gio::FileType::Directory); let size_opt = (!is_dir).then_some(info.size() as u64); let mut children_opt = None; if is_dir { if let Some(path) = file.child(&name).path() { //TODO: calculate children in the background (and make it cancellable?) match std::fs::read_dir(&path) { Ok(entries) => { children_opt = Some(entries.count()); } Err(err) => { log::warn!("failed to read directory {}: {}", path.display(), err); children_opt = Some(0); } } } else { children_opt = Some(0); } } ItemMetadata::GvfsPath { mtime, size_opt, children_opt, } } else { ItemMetadata::SimpleDir { entries: 0 } }; let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { let file_icon = |size| { info.icon() .as_ref() .and_then(|icon| gio_icon_to_path(icon, size)) .map(widget::icon::from_path) .unwrap_or( widget::icon::from_name(if metadata.is_dir() { "folder" } else { "text-x-generic" }) .size(size) .handle(), ) }; ( //TODO: get mime from content_type? "inode/directory".parse().unwrap(), file_icon(sizes.grid()), file_icon(sizes.list()), file_icon(sizes.list_condensed()), ) }; // Check if item is hidden let hidden = name.starts_with('.') || info.boolean(gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) || hidden_files.contains(&name); items.push(tab::Item { name, is_mount_point: false, display_name, metadata, hidden, location_opt: Some(location), image_dimensions: None, mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), selected: false, highlighted: false, overlaps_drag_rect: false, //TODO: scan directory size on gvfs mounts? dir_size: DirSize::NotDirectory, cut: false, }); } Ok(items) } fn dir_info(uri: &str) -> Result<(String, String, Option), glib::Error> { let (resolved_uri, file) = resolve_uri(uri); let info = file.query_info( gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE, )?; Ok((resolved_uri, info.display_name().into(), file.path())) } fn mount_op( uri: String, event_tx: std::sync::Weak>, ) -> gio::MountOperation { let mount_op = gio::MountOperation::new(); mount_op.connect_ask_password( move |mount_op, message, default_user, default_domain, flags| { let auth = MounterAuth { message: message.to_string(), username_opt: flags .contains(gio::AskPasswordFlags::NEED_USERNAME) .then(|| default_user.to_string()), domain_opt: flags .contains(gio::AskPasswordFlags::NEED_DOMAIN) .then(|| default_domain.to_string()), password_opt: flags .contains(gio::AskPasswordFlags::NEED_PASSWORD) .then(String::new), remember_opt: flags .contains(gio::AskPasswordFlags::SAVING_SUPPORTED) .then_some(false), anonymous_opt: flags .contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) .then_some(false), }; let (auth_tx, mut auth_rx) = mpsc::channel(1); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)); } //TODO: async recv? if let Some(auth) = auth_rx.blocking_recv() { if auth.anonymous_opt == Some(true) { mount_op.set_anonymous(true); } else { mount_op.set_username(auth.username_opt.as_deref()); mount_op.set_domain(auth.domain_opt.as_deref()); mount_op.set_password(auth.password_opt.as_deref()); if auth.remember_opt == Some(true) { mount_op.set_password_save(gio::PasswordSave::Permanently); } } mount_op.reply(gio::MountOperationResult::Handled); } else { mount_op.reply(gio::MountOperationResult::Aborted); } }, ); mount_op } enum Cmd { Items(IconSizes, mpsc::Sender), Rescan, Mount( MounterItem, tokio::sync::oneshot::Sender>, ), NetworkDrive(String, tokio::sync::oneshot::Sender>), NetworkScan( String, IconSizes, mpsc::Sender, String>>, ), DirInfo( String, mpsc::Sender), glib::Error>>, ), Unmount(MounterItem), } enum Event { Changed, Items(MounterItems), MountResult(MounterItem, Result), NetworkAuth(String, MounterAuth, mpsc::Sender), NetworkResult(String, Result), } #[derive(Clone, Debug)] enum ItemKind { Mount, Volume, } //TODO: better method of matching items #[derive(Clone, Debug)] pub struct Item { uri: String, kind: ItemKind, index: usize, name: String, is_mounted: bool, is_remote: bool, icon_opt: Option, icon_symbolic_opt: Option, path_opt: Option, } impl Item { pub fn name(&self) -> String { self.name.clone() } pub const fn is_mounted(&self) -> bool { self.is_mounted } pub const fn is_remote(&self) -> bool { self.is_remote } pub fn uri(&self) -> String { self.uri.clone() } pub fn icon(&self, symbolic: bool) -> Option { if symbolic { self.icon_symbolic_opt.as_ref() } else { self.icon_opt.as_ref() } .map(|icon| widget::icon::from_path(icon.clone())) } pub fn path(&self) -> Option { self.path_opt.clone() } } pub struct Gvfs { command_tx: mpsc::UnboundedSender, event_rx: Arc>, } impl Gvfs { pub fn new() -> Self { //TODO: switch to using gvfs-zbus which will better integrate with async rust let (command_tx, mut command_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = crate::channel::channel(); let event_tx = Arc::new(event_tx); std::thread::spawn(move || { let main_loop = glib::MainLoop::new(None, false); main_loop.context().spawn_local(async move { let event_tx = Arc::downgrade(&event_tx); let monitor = gio::VolumeMonitor::get(); { let event_tx = event_tx.clone(); monitor.connect_mount_changed(move |_monitor, mount| { log::info!("mount changed {}", MountExt::name(mount)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } { let event_tx = event_tx.clone(); monitor.connect_mount_added(move |_monitor, mount| { log::info!("mount added {}", MountExt::name(mount)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } { let event_tx = event_tx.clone(); monitor.connect_mount_removed(move |_monitor, mount| { log::info!("mount removed {}", MountExt::name(mount)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } { let event_tx = event_tx.clone(); monitor.connect_volume_changed(move |_monitor, volume| { log::info!("volume changed {}", VolumeExt::name(volume)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } { let event_tx = event_tx.clone(); monitor.connect_volume_added(move |_monitor, volume| { log::info!("volume added {}", VolumeExt::name(volume)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } { let event_tx = event_tx.clone(); monitor.connect_volume_removed(move |_monitor, volume| { log::info!("volume removed {}", VolumeExt::name(volume)); if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed); } }); } while let Some(command) = command_rx.recv().await { match command { Cmd::Items(sizes, items_tx) => { items_tx.send(items(&monitor, sizes)).await.unwrap(); } Cmd::Rescan => { let Some(event_tx) = event_tx.upgrade() else { return; }; event_tx.send(Event::Items(items(&monitor, IconSizes::default()))); } Cmd::Mount(mounter_item, complete_tx) => { let MounterItem::Gvfs(ref item) = mounter_item else { _ = complete_tx.send(Err(anyhow::anyhow!("No mounter item"))); continue }; let ItemKind::Volume = item.kind else { _ = complete_tx.send(Err(anyhow::anyhow!("No mounter volume"))); continue }; for (i, volume) in monitor.volumes().into_iter().enumerate() { if i != item.index { continue; } let name = VolumeExt::name(&volume); if item.name != name { log::warn!("trying to mount volume {} failed: name is {:?} when {:?} was expected", i, name, item.name); continue; } log::info!("mount {name}"); //TODO: do not use name as a URI for mount_op let mount_op = mount_op(name.to_string(), event_tx.clone()); let event_tx = event_tx.clone(); let mounter_item = mounter_item.clone(); let volume_for_callback = volume.clone(); VolumeExt::mount( &volume, gio::MountMountFlags::NONE, Some(&mount_op), gio::Cancellable::NONE, move |res| { log::info!("mount {name}: result {res:?}"); // Update the mounter_item with mount information after successful mount let mut updated_item = mounter_item.clone(); if res.is_ok() && let MounterItem::Gvfs(ref mut item) = updated_item && let Some(mount) = volume_for_callback.get_mount() { let root = MountExt::root(&mount); item.path_opt = root.path(); item.is_mounted = true; // Query if remote item.is_remote = root .query_filesystem_info( gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE, gio::Cancellable::NONE, ) .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(())); Ok(true) }, Err(err) => { _ = complete_tx.send(Err(anyhow::anyhow!("{err:?}"))); match err.kind::() { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) }} })); }, ); break; } } Cmd::NetworkDrive(uri, result_tx) => { let file = gio::File::for_uri(&uri); let mount_op = mount_op(uri.clone(), event_tx.clone()); let event_tx = event_tx.clone(); file.mount_enclosing_volume( gio::MountMountFlags::NONE, Some(&mount_op), 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(())); Ok(true)}, Err(err) => { _ = result_tx.send(Err(anyhow::anyhow!("{err:?}"))); match err.kind::() { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) }} })); } ); } Cmd::NetworkScan(uri, sizes, items_tx) => { let (resolved_uri, file) = resolve_uri(&uri); let needs_mount = resolved_uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) { Ok(_) => false, Err(err) => matches!(err.kind::(), Some(gio::IOErrorEnum::NotMounted)) }; if needs_mount { let mount_op = mount_op(resolved_uri.clone(), event_tx.clone()); let event_tx = event_tx.clone(); file.mount_enclosing_volume( gio::MountMountFlags::empty(), Some(&mount_op), gio::Cancellable::NONE, move |res| { log::info!("network scan mounted {resolved_uri}: result {res:?}"); // 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) }, Err(err) => match err.kind::() { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{err}")) } })); } ); } else { items_tx.send(network_scan(&uri, sizes)).await.unwrap(); } } Cmd::DirInfo(uri, result_tx) => { result_tx.send(dir_info(&uri)).await.unwrap(); } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Mount = item.kind else { continue }; for (i, mount) in monitor.mounts().into_iter().enumerate() { if i != item.index { continue; } let name = MountExt::name(&mount); if item.name != name { log::warn!("trying to unmount mount {} failed: name is {:?} when {:?} was expected", i, name, item.name); continue; } if MountExt::can_eject(&mount) { log::info!("eject {name}"); MountExt::eject_with_operation( &mount, gio::MountUnmountFlags::NONE, gio::MountOperation::NONE, gio::Cancellable::NONE, move |result| { log::info!("eject {name}: result {result:?}"); }, ); } else { log::info!("unmount {name}"); MountExt::unmount_with_operation( &mount, gio::MountUnmountFlags::NONE, gio::MountOperation::NONE, gio::Cancellable::NONE, move |result| { log::info!("unmount {name}: result {result:?}"); }, ); } } } } } }); main_loop.run(); }); Self { command_tx, event_rx: Arc::new(event_rx), } } } impl Mounter for Gvfs { fn items(&self, sizes: IconSizes) -> Option { let (items_tx, mut items_rx) = mpsc::channel(1); self.command_tx.send(Cmd::Items(sizes, items_tx)).unwrap(); items_rx.blocking_recv() } fn mount(&self, item: MounterItem) -> Task<()> { let command_tx = self.command_tx.clone(); Task::perform( async move { let (res_tx, res_rx) = tokio::sync::oneshot::channel(); command_tx.send(Cmd::Mount(item, res_tx)).unwrap(); res_rx.await }, |x| { if let Err(err) = x { log::error!("{err:?}"); } }, ) } fn network_drive(&self, uri: String) -> Task<()> { let command_tx = self.command_tx.clone(); Task::perform( async move { let (res_tx, res_rx) = tokio::sync::oneshot::channel(); command_tx.send(Cmd::NetworkDrive(uri, res_tx)).unwrap(); res_rx.await }, |x| { if let Err(err) = x { log::error!("{err:?}"); } }, ) } fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option, String>> { let (items_tx, mut items_rx) = mpsc::channel(1); self.command_tx .send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx)) .unwrap(); items_rx.blocking_recv() } fn dir_info(&self, uri: &str) -> Option<(String, String, Option)> { let (result_tx, mut result_rx) = mpsc::channel(1); self.command_tx .send(Cmd::DirInfo(uri.to_string(), result_tx)) .unwrap(); result_rx.blocking_recv().and_then(|res| res.ok()) } fn unmount(&self, item: MounterItem) -> Task<()> { let command_tx = self.command_tx.clone(); Task::future(async move { command_tx.send(Cmd::Unmount(item)).unwrap(); }) } fn subscription(&self) -> Subscription { let command_tx = self.command_tx.clone(); let event_rx = self.event_rx.clone(); struct Wrapper { command_tx: mpsc::UnboundedSender, event_rx: Arc>, } impl Hash for Wrapper { fn hash(&self, state: &mut H) { TypeId::of::().hash(state); } } Subscription::run_with( Wrapper { command_tx, event_rx, }, |Wrapper { command_tx, event_rx, }| { let command_tx = command_tx.clone(); let event_rx = event_rx.clone(); stream::channel( 1, move |mut output: cosmic::iced::futures::channel::mpsc::Sender< MounterMessage, >| async move { command_tx.send(Cmd::Rescan).unwrap(); while let Some(event) = event_rx.recv().await { match event { Event::Changed => command_tx.send(Cmd::Rescan).unwrap(), Event::Items(items) => { output.send(MounterMessage::Items(items)).await.unwrap(); } Event::MountResult(item, res) => output .send(MounterMessage::MountResult(item, res)) .await .unwrap(), Event::NetworkAuth(uri, auth, auth_tx) => output .send(MounterMessage::NetworkAuth(uri, auth, auth_tx)) .await .unwrap(), Event::NetworkResult(uri, res) => output .send(MounterMessage::NetworkResult(uri, res)) .await .unwrap(), } } pending().await }, ) }, ) } } ================================================ FILE: src/mounter/mod.rs ================================================ 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; use crate::tab; #[cfg(feature = "gvfs")] mod gvfs; #[derive(Clone)] pub struct MounterAuth { pub message: String, pub username_opt: Option, pub domain_opt: Option, pub password_opt: Option, pub remember_opt: Option, pub anonymous_opt: Option, } // Custom debug for MounterAuth to hide password impl fmt::Debug for MounterAuth { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MounterAuth") .field("username_opt", &self.username_opt) .field("domain_opt", &self.domain_opt) .field( "password_opt", if self.password_opt.is_some() { &"Some(*)" } else { &"None" }, ) .field("remember_opt", &self.remember_opt) .field("anonymous_opt", &self.anonymous_opt) .finish() } } #[derive(Clone, Debug)] pub enum MounterItem { #[cfg(feature = "gvfs")] Gvfs(gvfs::Item), #[allow(dead_code)] None, } impl MounterItem { pub fn name(&self) -> String { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.name(), Self::None => unreachable!(), } } pub fn uri(&self) -> String { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.uri(), Self::None => unreachable!(), } } pub fn is_mounted(&self) -> bool { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.is_mounted(), Self::None => unreachable!(), } } pub fn icon(&self, symbolic: bool) -> Option { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.icon(symbolic), Self::None => unreachable!(), } } pub fn path(&self) -> Option { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.path(), Self::None => unreachable!(), } } pub fn is_remote(&self) -> bool { match self { #[cfg(feature = "gvfs")] Self::Gvfs(item) => item.is_remote(), Self::None => unreachable!(), } } } pub type MounterItems = Vec; #[derive(Clone, Debug)] pub enum MounterMessage { Items(MounterItems), MountResult(MounterItem, Result), NetworkAuth(String, MounterAuth, mpsc::Sender), NetworkResult(String, Result), } pub trait Mounter: Send + Sync { fn items(&self, sizes: IconSizes) -> Option; //TODO: send result fn mount(&self, item: MounterItem) -> Task<()>; fn network_drive(&self, uri: String) -> Task<()>; fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option, String>>; fn dir_info(&self, uri: &str) -> Option<(String, String, Option)>; fn unmount(&self, item: MounterItem) -> Task<()>; fn subscription(&self) -> Subscription; } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct MounterKey(pub &'static str); pub type MounterMap = BTreeMap>; pub type Mounters = Arc; pub fn mounters() -> Mounters { #[allow(unused_mut)] let mut mounters = MounterMap::new(); #[cfg(feature = "gvfs")] { mounters.insert(MounterKey("gvfs"), Box::new(gvfs::Gvfs::new())); } Mounters::new(mounters) } pub static MOUNTERS: LazyLock = LazyLock::new(mounters); ================================================ FILE: src/mouse_area.rs ================================================ //! A container for capturing mouse events. use std::time::Instant; use crate::tab::DOUBLE_CLICK_DURATION; 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)] pub struct MouseArea<'a, Message> { id: Id, content: Element<'a, Message>, on_auto_scroll: Option>>, on_drag: Option>>, on_double_click: Option>>, on_press: Option>>, on_drag_end: Option>>, on_release: Option>>, on_resize: Option>>, on_right_press: Option>>, on_right_press_no_capture: bool, on_right_press_window_position: bool, on_right_release: Option>>, on_middle_press: Option>>, on_middle_release: Option>>, on_back_press: Option>>, on_back_release: Option>>, on_forward_press: Option>>, on_forward_release: Option>>, on_scroll: Option>>, on_enter: Option>>, on_exit: Option>>, show_drag_rect: bool, } impl<'a, Message> MouseArea<'a, Message> { /// The message to emit when auto scroll changes. #[must_use] pub fn on_auto_scroll(mut self, message: impl OnAutoScroll<'a, Message>) -> Self { self.on_auto_scroll = Some(Box::new(message)); self } /// The message to emit when a drag is initiated. #[must_use] pub fn on_drag(mut self, message: impl OnDrag<'a, Message>) -> Self { self.on_drag = Some(Box::new(message)); self } /// The message to emit when a drag ends. #[must_use] pub fn on_drag_end(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_drag_end = Some(Box::new(message)); self } /// The message to emit on a double click. #[must_use] pub fn on_double_click(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_double_click = Some(Box::new(message)); self } /// The message to emit on a left button press. #[must_use] pub fn on_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_press = Some(Box::new(message)); self } /// The message to emit on a left button release. #[must_use] pub fn on_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_release = Some(Box::new(message)); self } /// The message to emit on resizing. #[must_use] pub fn on_resize(mut self, message: impl OnResize<'a, Message>) -> Self { self.on_resize = Some(Box::new(message)); self } /// The message to emit on a right button press. #[must_use] pub fn on_right_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_right_press = Some(Box::new(message)); self } /// on_right_press will not capture input #[must_use] pub fn on_right_press_no_capture(mut self) -> Self { self.on_right_press_no_capture = true; self } /// Only on wayland, on_right_press will provide window position instead of widget relative #[must_use] pub fn wayland_on_right_press_window_position(mut self) -> Self { #[cfg(feature = "wayland")] { self.on_right_press_window_position = true; } self } /// on_right_press will provide window position instead of widget relative #[must_use] pub fn on_right_press_window_position(mut self) -> Self { self.on_right_press_window_position = true; self } /// The message to emit on a right button release. #[must_use] pub fn on_right_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_right_release = Some(Box::new(message)); self } /// The message to emit on a middle button press. #[must_use] pub fn on_middle_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_middle_press = Some(Box::new(message)); self } /// The message to emit on a middle button release. #[must_use] pub fn on_middle_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_middle_release = Some(Box::new(message)); self } /// The message to emit on a back button press. #[must_use] pub fn on_back_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_back_press = Some(Box::new(message)); self } /// The message to emit on a back button release. #[must_use] pub fn on_back_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_back_release = Some(Box::new(message)); self } /// The message to emit on a forward button press. #[must_use] pub fn on_forward_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_forward_press = Some(Box::new(message)); self } /// The message to emit on a forward button release. #[must_use] pub fn on_forward_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self { self.on_forward_release = Some(Box::new(message)); self } /// The message to emit on a scroll. #[must_use] pub fn on_scroll(mut self, message: impl OnScroll<'a, Message>) -> Self { self.on_scroll = Some(Box::new(message)); self } /// The message to emit when a mouse enters the area. #[must_use] pub fn on_enter(mut self, message: impl OnEnterExit<'a, Message>) -> Self { self.on_enter = Some(Box::new(message)); self } /// The message to emit when a mouse exits the area. #[must_use] pub fn on_exit(mut self, message: impl OnEnterExit<'a, Message>) -> Self { self.on_exit = Some(Box::new(message)); self } #[must_use] pub const fn show_drag_rect(mut self, show_drag_rect: bool) -> Self { self.show_drag_rect = show_drag_rect; self } /// Sets the widget's unique identifier. #[must_use] pub fn with_id(mut self, id: Id) -> Self { self.id = id; self } } pub trait OnAutoScroll<'a, Message>: Fn(Option) -> Message + 'a {} impl<'a, Message, F> OnAutoScroll<'a, Message> for F where F: Fn(Option) -> Message + 'a {} pub trait OnMouseButton<'a, Message>: Fn(Option) -> Message + 'a {} impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option) -> Message + 'a {} pub trait OnDrag<'a, Message>: Fn(Option) -> Message + 'a {} impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option) -> Message + 'a {} pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {} impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {} pub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta) -> Option + 'a {} impl<'a, Message, F> OnScroll<'a, Message> for F where F: Fn(mouse::ScrollDelta) -> Option + 'a { } pub trait OnEnterExit<'a, Message>: Fn() -> Message + 'a {} impl<'a, Message, F> OnEnterExit<'a, Message> for F where F: Fn() -> Message + 'a {} /// Local state of the [`MouseArea`]. #[derive(Default)] struct State { last_auto_scroll: Option, last_position: Option, last_virtual_position: Option, drag_initiated: Option, prev_click: Option<(mouse::Click, Instant)>, viewport: Option, } impl State { fn drag_rect(&self, cursor: mouse::Cursor) -> Option { if let Some(drag_source) = self.drag_initiated && let Some(position) = cursor.position().or(self.last_virtual_position) && position.distance(drag_source) > 1.0 { let min_x = drag_source.x.min(position.x); let max_x = drag_source.x.max(position.x); let min_y = drag_source.y.min(position.y); let max_y = drag_source.y.max(position.y); return Some(Rectangle::new( Point::new(min_x, min_y), Size::new(max_x - min_x, max_y - min_y), )); } None } fn click(&mut self, pos: Point) -> mouse::Click { let now = Instant::now(); let new = if let Some((prev_click, prev_time)) = self.prev_click.take() { if now.duration_since(prev_time) < DOUBLE_CLICK_DURATION { match prev_click.kind() { mouse::click::Kind::Single => { mouse::Click::new(pos, mouse::Button::Left, Some(prev_click)) } mouse::click::Kind::Double => { mouse::Click::new(pos, mouse::Button::Left, Some(prev_click)) } mouse::click::Kind::Triple => { mouse::Click::new(pos, mouse::Button::Left, Some(prev_click)) } } } else { mouse::Click::new(pos, mouse::Button::Left, None) } } else { mouse::Click::new(pos, mouse::Button::Left, None) }; self.prev_click = Some((new, now)); new } } impl<'a, Message> MouseArea<'a, Message> { /// Creates a [`MouseArea`] with the given content. pub fn new(content: impl Into>) -> Self { MouseArea { id: Id::unique(), content: content.into(), on_auto_scroll: None, on_drag: None, on_drag_end: None, on_double_click: None, on_press: None, on_release: None, on_resize: None, on_right_press: None, on_right_press_no_capture: false, on_right_press_window_position: false, on_right_release: None, on_middle_press: None, on_middle_release: None, on_back_press: None, on_back_release: None, on_forward_press: None, on_forward_release: None, on_enter: None, on_exit: None, on_scroll: None, show_drag_rect: false, } } } impl Widget for MouseArea<'_, Message> where Message: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&mut self, tree: &mut Tree) { tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); if shell.is_event_captured() { return; } update( self, event, layout, cursor, shell, tree.state.downcast_mut::(), viewport, ); } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, renderer_style, layout, cursor, viewport, ); if self.show_drag_rect { let state = tree.state.downcast_ref::(); if let Some(bounds) = state.drag_rect(cursor) { let cosmic = theme.cosmic(); let mut bg_color = cosmic.accent_color(); //TODO: get correct alpha bg_color.alpha = 0.2; renderer.start_layer(*viewport); renderer.fill_quad( Quad { bounds, border: Border { color: cosmic.accent_color().into(), width: 1.0, radius: cosmic.radius_xs().into(), }, ..Default::default() }, Color::from(bg_color), ); renderer.end_layer(); } } } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } fn drag_destinations( &self, state: &Tree, layout: Layout<'_>, renderer: &Renderer, dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles, ) { self.content.as_widget().drag_destinations( &state.children[0], layout, renderer, dnd_rectangles, ); } fn id(&self) -> Option { Some(self.id.clone()) } fn set_id(&mut self, id: Id) { self.id = id; } } impl<'a, Message> From> for Element<'a, Message> where Message: 'a + Clone, Renderer: 'a + renderer::Renderer, Theme: 'a, { fn from(area: MouseArea<'a, Message>) -> Self { Element::new(area) } } /// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`] /// accordingly. fn update( widget: &mut MouseArea<'_, Message>, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, viewport: &Rectangle, ) { let offset = layout.virtual_offset(); let layout_bounds = layout.bounds(); let viewport_changed = state.viewport != Some(*viewport); if let Some(message) = widget.on_resize.as_ref() && viewport_changed { shell.publish(message(*viewport)); } state.viewport = Some(*viewport); let should_check_hover = viewport_changed || matches!( event, Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Mouse(mouse::Event::WheelScrolled { .. }) ); if should_check_hover { let position_in = cursor.position_in(layout_bounds); match (position_in, state.last_position) { (None, Some(_)) => { if let Some(message) = widget.on_exit.as_ref() { shell.publish(message()); } } (Some(_), None) => { if let Some(message) = widget.on_enter.as_ref() { shell.publish(message()); } } _ => {} } state.last_position = position_in; } if let Event::Mouse(mouse::Event::CursorMoved { position }) = event { let virtual_position = Point::new( viewport.x - layout_bounds.x + position.x, viewport.y - layout_bounds.y + position.y, ); state.last_virtual_position = Some(virtual_position); if let Some(message) = widget.on_auto_scroll.as_ref() { let auto_scroll = if state.drag_initiated.is_some() { let bottom = viewport.y; let top = viewport.y + viewport.height; if virtual_position.y < bottom { Some(virtual_position.y - bottom) } else if virtual_position.y > top { Some(virtual_position.y - top) } else { None } } else { None }; if state.last_auto_scroll != auto_scroll { shell.publish(message(auto_scroll)); state.last_auto_scroll = auto_scroll; } } } if state.drag_initiated.is_none() && !cursor.is_over(layout_bounds) { return; } if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event { let click = state.click(cursor.position_in(layout_bounds).unwrap_or_default()); match click.kind() { click::Kind::Single => { if let Some(message) = widget.on_press.as_ref() { shell.publish(message(cursor.position_in(layout_bounds))); } } click::Kind::Double => { if let Some(message) = widget.on_double_click.as_ref() { shell.publish(message(cursor.position_in(layout_bounds))); } } click::Kind::Triple => { // TODO what to do here if let Some(message) = widget.on_press.as_ref() { shell.publish(message(cursor.position_in(layout_bounds))); } } } if widget.on_drag.is_some() { state.drag_initiated = cursor.position(); } if widget.on_press.is_some() { shell.capture_event(); return; } } let distance_dragged = state .drag_initiated .map(|initiated| initiated.distance(cursor.position().unwrap_or_default())) .unwrap_or_default(); if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) ) && distance_dragged > 1.0 { state.drag_initiated = None; state.prev_click = None; if let Some(message) = widget.on_drag_end.as_ref() { shell.publish(message(cursor.position_in(layout_bounds))); } } let recent_click = state .prev_click .as_ref() .is_some_and(|(_, i)| Instant::now().duration_since(*i) <= DOUBLE_CLICK_DURATION); if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) ) && state.prev_click.is_some() { if !recent_click { state.prev_click = None; return; } state.drag_initiated = None; if let Some(message) = widget.on_release.as_ref() { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } } if let Some(message) = widget.on_right_press.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) ) { let point_opt = if widget.on_right_press_window_position { cursor.position_over(layout_bounds).map(|mut p| { p.x -= offset.x; p.y -= offset.y; p }) } else { cursor.position_in(layout_bounds) }; shell.publish(message(point_opt)); if widget.on_right_press_no_capture { return; } shell.capture_event(); return; } if let Some(message) = widget.on_right_release.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_middle_press.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_middle_release.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_back_press.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_back_release.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_forward_press.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(message) = widget.on_forward_release.as_ref() && matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward)) ) { shell.publish(message(cursor.position_in(layout_bounds))); shell.capture_event(); return; } if let Some(on_scroll) = widget.on_scroll.as_ref() && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event && let Some(message) = on_scroll(*delta) { shell.publish(message); shell.capture_event(); return; } if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) { shell.publish(message(drag_rect.intersection(&layout_bounds).map( |mut rect| { rect.x -= layout_bounds.x; rect.y -= layout_bounds.y; rect }, ))); } } ================================================ FILE: src/operation/controller.rs ================================================ 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, IntoPrimitive, TryFromPrimitive)] #[repr(u16)] pub enum ControllerState { Cancelled, Failed, Paused, Running, } #[derive(Debug)] struct ControllerInner { state: AtomicU16, progress: AtomicF32, notify: Notify, } #[derive(Debug)] pub struct Controller { primary: bool, inner: Arc, } impl Default for Controller { fn default() -> Self { Self { primary: true, inner: Arc::new(ControllerInner { state: AtomicU16::new(ControllerState::Running.into()), progress: AtomicF32::new(0.0), notify: Notify::new(), }), } } } impl Controller { pub async fn check(&self) -> Result<(), ControllerState> { loop { match self.state() { ControllerState::Cancelled => return Err(ControllerState::Cancelled), ControllerState::Failed => return Err(ControllerState::Failed), ControllerState::Paused => (), ControllerState::Running => return Ok(()), } self.inner.notify.notified().await; } } pub fn progress(&self) -> f32 { self.inner.progress.load(atomic::Ordering::Relaxed) } pub fn set_progress(&self, progress: f32) { self.inner .progress .swap(progress, atomic::Ordering::Relaxed); } pub fn state(&self) -> ControllerState { ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed)) .unwrap_or(ControllerState::Failed) } pub fn set_state(&self, state: ControllerState) { self.inner .state .store(state.into(), atomic::Ordering::Relaxed); self.inner.notify.notify_waiters(); } pub fn is_cancelled(&self) -> bool { matches!(self.state(), ControllerState::Cancelled) } pub fn cancel(&self) { self.set_state(ControllerState::Cancelled); } pub fn is_failed(&self) -> bool { matches!(self.state(), ControllerState::Failed) } pub fn is_paused(&self) -> bool { matches!(self.state(), ControllerState::Paused) } pub fn pause(&self) { 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); } } } impl Clone for Controller { fn clone(&self) -> Self { Self { primary: false, inner: self.inner.clone(), } } } impl Drop for Controller { fn drop(&mut self) { // Cancel operations if primary controller is dropped and controller is still running if self.primary && self.state() != ControllerState::Failed { self.cancel(); } } } ================================================ FILE: src/operation/mod.rs ================================================ 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; pub use self::controller::{Controller, ControllerState}; pub mod controller; pub use notifiers::*; mod notifiers; pub use self::reader::OpReader; pub mod reader; use self::recursive::{Context, Method}; pub mod recursive; async fn handle_replace( msg_tx: Arc>>, file_from: PathBuf, file_to: PathBuf, multiple: bool, conflict_count: usize, ) -> ReplaceResult { let item_from = match tab::item_from_path(file_from, IconSizes::default()) { Ok(ok) => Box::new(ok), Err(err) => { log::warn!("{err}"); return ReplaceResult::Cancel; } }; let item_to = match tab::item_from_path(file_to, IconSizes::default()) { Ok(ok) => Box::new(ok), Err(err) => { log::warn!("{err}"); return ReplaceResult::Cancel; } }; let (tx, mut rx) = mpsc::channel(1); let _ = msg_tx .lock() .await .send(Message::DialogPush( DialogPage::Replace { from: item_from, to: item_to, multiple, apply_to_all: false, conflict_count, tx, }, Some(REPLACE_BUTTON_ID.clone()), )) .await; rx.recv().await.unwrap_or(ReplaceResult::Cancel) } fn get_directory_name(file_name: &str) -> &str { // TODO: Chain with COMPOUND_EXTENSIONS once more formats are supported for ext in crate::archive::SUPPORTED_EXTENSIONS { if let Some(stripped) = file_name.strip_suffix(ext) { return stripped; } } file_name } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum ReplaceResult { Replace(bool), KeepBoth, Skip(bool), Cancel, } async fn copy_or_move( paths: Vec, to: PathBuf, method: Method, msg_tx: &Arc>>, controller: Controller, ) -> Result { let msg_tx = msg_tx.clone(); let controller_c = controller.clone(); compio::runtime::spawn(async move { let controller = controller_c; log::info!( "{} {:?} to {}", match method { Method::Copy => "Copy", Method::Move { .. } => "Move", }, paths, to.display() ); // Handle duplicate file names by renaming paths let from_to_pairs_iter = paths .into_iter() .zip(std::iter::repeat(to.as_path())) .filter_map(|(from, to)| { if matches!(from.parent(), Some(parent) if parent == to) && matches!(method, Method::Copy) { // `from`'s parent is equal to `to` which means we're copying to the same // directory (duplicating files) let to = copy_unique_path(&from, to); Some((from, to)) } else if let Some(name) = from.file_name() { let to = to.join(name); Some((from, to)) } else { //TODO: how to handle from missing file name? None } }); // Attempt quick and simple renames //TODO: allow rename to be used for directories in recursive context? 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)); } 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::>() .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()); { let controller = controller.clone(); context = context.on_progress(move |_op, progress| { let item_progress = match progress.total_bytes { Some(total_bytes) => { if total_bytes == 0 { 1.0 } else { progress.current_bytes as f32 / total_bytes as f32 } } None => 0.0, }; let total_progress = (item_progress + progress.current_ops as f32) / progress.total_ops as f32; controller.set_progress(total_progress); }); } { let msg_tx = msg_tx.clone(); context = context.on_replace(move |op, conflict_count| { let msg_tx = msg_tx.clone(); Box::pin(handle_replace( msg_tx, op.from.clone(), op.to.clone(), true, conflict_count, )) }); } context .recursive_copy_or_move(from_to_pairs, method) .await?; Result::::Ok(context.op_sel) }) .await .map_err(wrap_compio_spawn_error)? } pub async fn sync_to_disk( written_files: Vec, target_dirs: std::collections::HashSet, ) { // Sync files to disk stream::iter(written_files.into_iter().map(|path| async move { if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await { let _ = file.sync_all().await; } })) .buffer_unordered(32) .collect::<()>() .await; // Sync directories to disk stream::iter(target_dirs.into_iter().map(|path| async move { if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await { let _ = dir.sync_all().await; } })) .buffer_unordered(16) .collect::<()>() .await; } pub fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { // List of compound extensions to check const COMPOUND_EXTENSIONS: &[&str] = &[ ".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz", ".tar.lzma", ".tar.sz", ".tar.lzo", ".tar.br", ".tar.Z", ".tar.pz", ]; let mut to = to.to_owned(); if let Some(file_name) = from.file_name().and_then(|name| name.to_str()) { let (stem, ext) = if from.is_dir() { (file_name.to_string(), None) } else { let file_name = file_name.to_string(); COMPOUND_EXTENSIONS .iter() .copied() .find(|&ext| file_name.ends_with(ext)) .map(|ext| { ( file_name.strip_suffix(ext).unwrap().to_string(), Some(ext[1..].to_string()), ) }) .unwrap_or_else(|| { from.file_stem() .and_then(|s| s.to_str()) .map_or((file_name, None), |stem| { ( stem.to_string(), from.extension() .and_then(|e| e.to_str()) .map(str::to_string), ) }) }) }; for n in 0.. { let new_name = if n == 0 { file_name.to_string() } else { match ext { Some(ref ext) => format!("{} ({} {}).{}", stem, fl!("copy_noun"), n, ext), None => format!("{} ({} {})", stem, fl!("copy_noun"), n), } }; to.push(&new_name); if !matches!(to.try_exists(), Ok(true)) { break; } // Continue if a copy with index exists to.pop(); } } to } fn file_name(path: &Path) -> Cow<'_, str> { path.file_name() .map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy()) } fn parent_name(path: &Path) -> Cow<'_, str> { let Some(parent) = path.parent() else { return fl!("unknown-folder").into(); }; file_name(parent) } fn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> { let Some(first_path) = paths.first() else { return fl!("unknown-folder").into(); }; let Some(parent) = first_path.parent() else { return fl!("unknown-folder").into(); }; for path in paths { //TODO: is it possible to have different parents, and what should be returned? if path.parent() != Some(parent) { return fl!("unknown-folder").into(); } } file_name(parent) } #[derive(Clone, Debug, Default)] pub struct OperationSelection { // Paths to ignore if they are already selected pub ignored: Vec, // Paths to select pub selected: Vec, } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum Operation { /// Compress files Compress { paths: Vec, to: PathBuf, archive_type: ArchiveType, password: Option, }, /// Copy items Copy { paths: Vec, to: PathBuf, }, /// Move items to the trash Delete { paths: Vec, }, /// Delete a path from the trash DeleteTrash { items: Vec, }, /// Empty the trash EmptyTrash, /// Uncompress files Extract { paths: Box<[PathBuf]>, to: PathBuf, password: Option, }, /// Move items Move { paths: Vec, to: PathBuf, cross_device_copy: bool, }, NewFile { path: PathBuf, }, NewFolder { path: PathBuf, }, /// Permanently delete items, skipping the trash PermanentlyDelete { paths: Box<[PathBuf]>, }, RemoveFromRecents { paths: Box<[PathBuf]>, }, Rename { from: PathBuf, to: PathBuf, }, /// Restore a path from the trash Restore { items: Vec, }, /// Set executable and launch SetExecutableAndLaunch { path: PathBuf, }, /// Set permissions SetPermissions { path: PathBuf, mode: u32, }, } #[derive(Clone, Debug)] pub enum OperationErrorType { Generic(String), PasswordRequired, } #[derive(Clone, Debug)] pub struct OperationError { pub kind: OperationErrorType, } impl OperationError { pub fn from_state(state: ControllerState, controller: &Controller) -> Self { let message = if state == ControllerState::Failed { controller.set_state(ControllerState::Failed); fl!("failed") } else { controller.cancel(); fl!("cancelled") }; Self { kind: OperationErrorType::Generic(message), } } pub fn from_err(err: T, controller: &Controller) -> Self { controller.set_state(ControllerState::Failed); Self { kind: OperationErrorType::Generic(err.to_string()), } } pub fn from_kind(kind: OperationErrorType, controller: &Controller) -> Self { controller.set_state(ControllerState::Failed); Self { kind } } pub fn from_msg(m: impl Into) -> Self { Self { kind: OperationErrorType::Generic(m.into()), } } } impl std::error::Error for OperationError {} impl std::fmt::Display for OperationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.kind { OperationErrorType::Generic(s) => s.fmt(f), OperationErrorType::PasswordRequired => f.write_str("Password required"), } } } impl Operation { pub fn pending_text(&self, ratio: f32, state: ControllerState) -> String { let percent = (ratio * 100.0) as i32; let progress = || match state { ControllerState::Running => fl!("progress", percent = percent), ControllerState::Paused => fl!("progress-paused", percent = percent), ControllerState::Cancelled => fl!("progress-cancelled", percent = percent), ControllerState::Failed => fl!("progress-failed", percent = percent), }; match self { Self::Compress { paths, to, .. } => fl!( "compressing", items = paths.len(), from = paths_parent_name(paths), to = file_name(to), progress = progress() ), Self::Copy { paths, to } => fl!( "copying", items = paths.len(), from = paths_parent_name(paths), to = file_name(to), progress = progress() ), Self::Delete { paths } => fl!( "moving", items = paths.len(), from = paths_parent_name(paths), to = fl!("trash"), progress = progress() ), Self::DeleteTrash { items } => { fl!("deleting", items = items.len(), progress = progress()) } Self::EmptyTrash => fl!("emptying-trash", progress = progress()), Self::Extract { paths, to, password: _, } => fl!( "extracting", items = paths.len(), from = paths_parent_name(paths), to = file_name(to), progress = progress() ), Self::Move { paths, to, .. } => fl!( "moving", items = paths.len(), from = paths_parent_name(paths), to = file_name(to), progress = progress() ), Self::NewFile { path } => fl!( "creating", name = file_name(path), parent = parent_name(path) ), Self::NewFolder { path } => fl!( "creating", name = file_name(path), parent = parent_name(path) ), Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()), Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } Self::RemoveFromRecents { paths } => fl!("removing-from-recents", items = paths.len()), Self::Restore { items } => fl!("restoring", items = items.len(), progress = progress()), Self::SetExecutableAndLaunch { path } => { fl!("setting-executable-and-launching", name = file_name(path)) } Self::SetPermissions { path, mode } => { fl!( "setting-permissions", name = file_name(path), mode = format!("{:#03o}", mode) ) } } } pub fn completed_text(&self) -> String { match self { Self::Compress { paths, to, .. } => fl!( "compressed", items = paths.len(), from = paths_parent_name(paths), to = file_name(to) ), Self::Copy { paths, to } => fl!( "copied", items = paths.len(), from = paths_parent_name(paths), to = file_name(to) ), Self::Delete { paths } => fl!( "moved", items = paths.len(), from = paths_parent_name(paths), to = fl!("trash") ), Self::DeleteTrash { items } => fl!("deleted", items = items.len()), Self::EmptyTrash => fl!("emptied-trash"), Self::Extract { paths, to, password: _, } => fl!( "extracted", items = paths.len(), from = paths_parent_name(paths), to = file_name(to) ), Self::Move { paths, to, .. } => fl!( "moved", items = paths.len(), from = paths_parent_name(paths), to = file_name(to) ), Self::NewFile { path } => fl!( "created", name = file_name(path), parent = parent_name(path) ), Self::NewFolder { path } => fl!( "created", name = file_name(path), parent = parent_name(path) ), Self::PermanentlyDelete { paths } => fl!("permanently-deleted", items = paths.len()), Self::RemoveFromRecents { paths } => fl!("removed-from-recents", items = paths.len()), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), Self::Restore { items } => fl!("restored", items = items.len()), Self::SetExecutableAndLaunch { path } => { fl!("set-executable-and-launched", name = file_name(path)) } Self::SetPermissions { path, mode } => { fl!( "set-permissions", name = file_name(path), mode = format!("{:#03o}", mode) ) } } } pub const fn show_progress_notification(&self) -> bool { // Long running operations show a progress notification match self { Self::Compress { .. } | Self::Copy { .. } | Self::Delete { .. } | Self::DeleteTrash { .. } | Self::EmptyTrash | Self::Extract { .. } | Self::Move { .. } | Self::PermanentlyDelete { .. } | Self::Restore { .. } => true, Self::NewFile { .. } | Self::NewFolder { .. } | Self::RemoveFromRecents { .. } | Self::Rename { .. } | Self::SetExecutableAndLaunch { .. } | Self::SetPermissions { .. } => false, } } pub fn toast(&self) -> Option { match self { Self::Compress { .. } => Some(self.completed_text()), Self::Delete { .. } => Some(self.completed_text()), Self::Extract { .. } => Some(self.completed_text()), //TODO: more toasts _ => None, } } /// Perform the operation pub async fn perform( self, msg_tx: &Arc>>, controller: Controller, ) -> Result { let controller_clone = controller.clone(); //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE let paths: Result = match self { Self::Compress { paths, to, archive_type, password, } => { let controller_c = controller.clone(); compio::runtime::spawn_blocking( move || -> Result { let controller = controller_c; let Some(relative_root) = to.parent() else { return Err(OperationError::from_err( format!("path {} has no parent directory", to.display()), &controller, )); }; let op_sel = OperationSelection { ignored: paths.clone(), selected: vec![to.clone()], }; let mut paths = paths; for path in &paths.clone() { if path.is_dir() { let new_paths_it = WalkDir::new(path).into_iter(); for entry in new_paths_it.skip(1) { let entry = entry .map_err(|e| OperationError::from_err(e, &controller))?; paths.push(entry.into_path()); } } } match archive_type { ArchiveType::Tgz => { let mut archive = fs::File::create(&to) .map(io::BufWriter::new) .map(|w| { flate2::write::GzEncoder::new( w, flate2::Compression::default(), ) }) .map(tar::Builder::new) .map_err(|e| OperationError::from_err(e, &controller))?; let total_paths = paths.len(); for (i, path) in paths.iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|e| OperationError::from_state(e, &controller)) })?; controller.set_progress((i as f32) / total_paths as f32); if let Some(relative_path) = path .strip_prefix(relative_root) .map_err(|e| OperationError::from_err(e, &controller))? .to_str() { archive .append_path_with_name(path, relative_path) .map_err(|e| { OperationError::from_err(e, &controller) })?; } } archive .finish() .map_err(|e| OperationError::from_err(e, &controller))?; } ArchiveType::Zip => { let mut archive = fs::File::create(&to) .map(io::BufWriter::new) .map(zip::ZipWriter::new) .map_err(|e| OperationError::from_err(e, &controller))?; let total_paths = paths.len(); let mut buffer = vec![0; 4 * 1024 * 1024]; for (i, path) in paths.iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller)) })?; controller.set_progress((i as f32) / total_paths as f32); let mut zip_options = zip::write::SimpleFileOptions::default(); if password.is_some() { zip_options = zip_options.with_aes_encryption( Aes256, password.as_deref().unwrap(), ); } if let Some(relative_path) = path .strip_prefix(relative_root) .map_err(|e| OperationError::from_err(e, &controller))? .to_str() { let mut file = fs::File::open(path).map_err(|e| { OperationError::from_err(e, &controller) })?; let metadata = file.metadata().map_err(|e| { OperationError::from_err(e, &controller) })?; 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); } #[cfg(unix)] { use std::os::unix::fs::MetadataExt; let mode = metadata.mode(); zip_options = zip_options.unix_permissions(mode); } if path.is_file() { let total = metadata.len(); if total >= 4 * 1024 * 1024 * 1024 { // The large file option must be enabled for files above 4 GiB zip_options = zip_options.large_file(true); } archive .start_file(relative_path, zip_options) .map_err(|e| { OperationError::from_err(e, &controller) })?; let mut current = 0; loop { futures::executor::block_on(async { controller.check().await.map_err(|s| { OperationError::from_state(s, &controller) }) })?; let count = file.read(&mut buffer).map_err(|e| { OperationError::from_err(e, &controller) })?; if count == 0 { break; } archive.write_all(&buffer[..count]).map_err( |e| OperationError::from_err(e, &controller), )?; current += count; let file_progress = current as f32 / total as f32; let total_progress = (i as f32 + file_progress) / total_paths as f32; controller.set_progress(total_progress); } } else { archive .add_directory(relative_path, zip_options) .map_err(|e| { OperationError::from_err(e, &controller) })?; } } } archive .finish() .map_err(|e| OperationError::from_err(e, &controller))?; } } Ok(op_sel) }, ) .await .map_err(wrap_compio_spawn_error)? } Self::Copy { paths, to } => { copy_or_move(paths, to, Method::Copy, msg_tx, controller).await } Self::Delete { paths } => { let total = paths.len(); for (i, path) in paths.into_iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller)) })?; controller.set_progress((i as f32) / (total as f32)); let _items_opt = compio::runtime::spawn_blocking(|| trash::delete(path)) .await .map_err(wrap_compio_spawn_error)?; //TODO: items_opt allows for easy restore } Ok(OperationSelection::default()) } Self::DeleteTrash { items } => { #[cfg(any( target_os = "windows", all( unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android") ) ))] { let controller_clone = controller.clone(); compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { let controller = controller_clone; let count = items.len(); for (i, item) in items.into_iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller)) })?; controller.set_progress(i as f32 / count as f32); trash::os_limited::purge_all([item]) .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(()) }) .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(OperationSelection::default()) } Self::EmptyTrash => { #[cfg(any( target_os = "windows", all( unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android") ) ))] { let controller_clone = controller.clone(); compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { let controller = controller_clone; let items = trash::os_limited::list() .map_err(|e| OperationError::from_err(e, &controller))?; let count = items.len(); let mut errors: Vec = Vec::new(); for (i, item) in items.into_iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller)) })?; if let Err(e) = trash::os_limited::purge_all([item]) { errors.push(e); } controller.set_progress(i as f32 / count as f32); } // Report errors at the end if !errors.is_empty() { log::warn!("Failed to purge {} items:", errors.len()); for e in &errors { log::warn!(" - {e}"); } // Return an error to signal partial failure return Err(OperationError::from_err( format!( "Failed to delete {} of {} items. Check log for details.", errors.len(), count ), &controller, )); } Ok(()) }) .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(OperationSelection::default()) } Self::Extract { paths, to, password, } => { let controller_clone = controller.clone(); compio::runtime::spawn_blocking( move || -> Result { let controller = controller_clone; let total_paths = paths.len(); let mut op_sel = OperationSelection::default(); for (i, path) in paths.iter().enumerate() { futures::executor::block_on(async { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller)) })?; controller.set_progress((i as f32) / total_paths as f32); if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { let dir_name = get_directory_name(file_name); let mut new_dir = to.join(dir_name); if new_dir.exists() && let Some(new_dir_parent) = new_dir.parent() { new_dir = copy_unique_path(&new_dir, new_dir_parent); } op_sel.ignored.push(path.clone()); op_sel.selected.push(new_dir.clone()); crate::archive::extract(path, &new_dir, &password, &controller)?; } } Ok(op_sel) }, ) } .await .map_err(wrap_compio_spawn_error)?, Self::Move { paths, to, cross_device_copy, } => { copy_or_move( paths, to, Method::Move { cross_device_copy }, msg_tx, controller, ) .await } Self::NewFolder { path } => { let controller_clone = controller.clone(); compio::runtime::spawn(async move { let controller = controller_clone; controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; compio::fs::create_dir(&path) .await .map_err(|e| OperationError::from_err(e, &controller))?; Result::<_, OperationError>::Ok(OperationSelection { ignored: Vec::new(), selected: vec![path], }) }) } .await .map_err(wrap_compio_spawn_error)?, Self::NewFile { path } => { let controller_clone = controller.clone(); compio::runtime::spawn(async move { let controller = controller_clone; controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; compio::fs::File::create(&path) .await .map_err(|e| OperationError::from_err(e, &controller))?; Result::<_, OperationError>::Ok(OperationSelection { ignored: Vec::new(), selected: vec![path], }) }) } .await .map_err(wrap_compio_spawn_error)?, Self::PermanentlyDelete { paths } => { let total = paths.len(); for (idx, path) in paths.into_iter().enumerate() { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; controller.set_progress((idx as f32) / (total as f32)); tokio::task::spawn_blocking(|| { if path.is_symlink() || path.is_file() { fs::remove_file(path) } else if path.is_dir() { fs::remove_dir_all(path) } else { Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "File to delete is not symlink, file or directory", )) } }) .await .map_err(|e| OperationError::from_err(e, &controller))? .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(OperationSelection::default()) } Self::RemoveFromRecents { paths } => { tokio::task::spawn_blocking(move || { let path_refs = paths.iter().map(PathBuf::as_path).collect::>(); recently_used_xbel::remove_recently_used(&path_refs) }) .await .map_err(|e| OperationError::from_err(e, &controller))? .map_err(|e| OperationError::from_err(e, &controller))?; Ok(OperationSelection::default()) } Self::Rename { from, to } => { let controller_clone = controller.clone(); compio::runtime::spawn(async move { let controller = controller_clone; controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; compio::fs::rename(&from, &to) .await .map_err(|e| OperationError::from_err(e, &controller))?; Result::<_, OperationError>::Ok(OperationSelection { ignored: vec![from], selected: vec![to], }) }) } .await .map_err(wrap_compio_spawn_error)?, #[cfg(target_os = "macos")] Self::Restore { .. } => { // TODO: add support for macos return Err(OperationError::from_msg( "Restoring from trash is not supported on macos", )); } #[cfg(not(target_os = "macos"))] Self::Restore { items } => { let total = items.len(); let mut paths = Vec::with_capacity(total); for (i, item) in items.into_iter().enumerate() { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; controller.set_progress((i as f32) / (total as f32)); paths.push(item.original_path()); compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item])) .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(OperationSelection { ignored: Vec::new(), selected: paths, }) } Self::SetExecutableAndLaunch { path } => { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; let controller_clone = controller.clone(); compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { let controller = controller_clone; //TODO: what to do on non-Unix systems? #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&path) .map_err(|e| OperationError::from_err(e, &controller))? .permissions(); let current_mode = perms.mode(); let new_mode = current_mode | 0o111; perms.set_mode(new_mode); fs::set_permissions(&path, perms) .map_err(|e| OperationError::from_err(e, &controller))?; } let mut command = std::process::Command::new(path); spawn_detached(&mut command) .map_err(|e| OperationError::from_err(e, &controller))?; Ok(()) }) .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; Ok(OperationSelection::default()) } Self::SetPermissions { path, mode } => { controller .check() .await .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)] { use std::os::unix::fs::PermissionsExt; let perms = fs::Permissions::from_mode(mode); fs::set_permissions(&path, perms) .map_err(|e| OperationError::from_err(e, &controller))?; } Ok(()) }) .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; Ok(OperationSelection { ignored: Vec::new(), selected: vec![path], }) } }; controller_clone.set_progress(1.0); paths } } #[track_caller] fn wrap_compio_spawn_error(err: Box) -> OperationError { log::error!( "compio runtime spawn failed: {}", std::backtrace::Backtrace::capture() ); // Preserve error if it's already an OperationError if let Ok(err) = err.downcast() { *err } else { OperationError::from_msg("compio runtime spawn failed") } } #[cfg(test)] mod tests { use std::fs::{self, File}; use std::io; use std::path::PathBuf; 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::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( paths: Vec, to: PathBuf, ) -> Result { let id = fastrand::u64(0..u64::MAX); let (tx, mut rx) = mpsc::channel(1); let paths_clone = paths.clone(); let to_clone = to.clone(); // Wrap this into its own future so that it may be polled concurerntly with the message handler. let handle_copy = async move { Operation::Copy { paths: paths_clone, to: to_clone, } .perform(&sync::Mutex::new(tx).into(), Controller::default()) .await }; // Concurrently handling messages will prevent the mpsc channel from blocking when full. let handle_messages = async move { while let Some(msg) = rx.next().await { match msg { Message::DialogPush(DialogPage::Replace { tx, .. }, _id_to_focus) => { debug!("[{id}] Replace request"); tx.send(ReplaceResult::Cancel) .await .expect("Sending a response to a replace request should succeed"); } _ => unreachable!( "Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation" ), } } }; future::join(handle_messages, handle_copy).await.1 } #[test(compio::test)] async fn copy_file_to_same_location() -> io::Result<()> { let fs = simple_fs(NUM_FILES, 0, 1, 0, NAME_LEN)?; let path = fs.path(); // Get the first file from the first directory let first_dir = filter_dirs(path)? .next() .expect("Should have at least one directory"); let first_file = filter_files(&first_dir)? .next() .expect("Should have at least one file"); // Duplicate that file let base_name = first_file .file_name() .and_then(|name| name.to_str()) .expect("File name exists and is valid"); debug!( "Duplicating {} in {}", first_file.display(), first_dir.display() ); operation_copy(vec![first_file.clone()], first_dir.clone()) .await .expect("Copy operation should have succeeded"); assert!(first_file.exists(), "Original file should still exist"); let expected = first_dir.join(format!("{base_name} ({} 1)", fl!("copy_noun"))); assert!(expected.exists(), "File should have been duplicated"); Ok(()) } #[test(compio::test)] async fn copy_file_with_extension_to_same_loc() -> io::Result<()> { let fs = empty_fs()?; let path = fs.path(); let base_name = "foo.txt"; let base_path = path.join(base_name); File::create(&base_path)?; debug!("Duplicating {}", base_path.display()); operation_copy(vec![base_path.clone()], path.to_owned()) .await .expect("Copy operation should have succeeded"); assert!(base_path.exists(), "Original file should still exist"); let expected = path.join(format!("foo ({} 1).txt", fl!("copy_noun"))); assert!(expected.exists(), "File should have been duplicated"); Ok(()) } #[test(compio::test)] async fn copy_dir_to_same_location() -> io::Result<()> { let fs = simple_fs(NUM_FILES, 0, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); // First directory path let first_dir = filter_dirs(path)? .next() .expect("Should have at least one directory"); let base_name = first_dir .file_name() .and_then(|name| name.to_str()) .expect("First directory exists and has a valid name"); debug!("Duplicating directory {}", first_dir.display()); operation_copy(vec![first_dir.clone()], path.to_owned()) .await .expect("Copy operation should have succeeded"); assert!(first_dir.exists(), "Original directory should still exist"); let expected = path.join(format!("{base_name} ({} 1)", fl!("copy_noun"))); assert!(expected.exists(), "Directory should have been duplicated"); Ok(()) } #[test(compio::test)] async fn copying_file_multiple_times_to_same_location() -> io::Result<()> { let fs = empty_fs()?; let path = fs.path(); let base_name = "cosmic"; let base_path = path.join(base_name); File::create(&base_path)?; for i in 1..5 { debug!("Duplicating {}", base_path.display()); operation_copy(vec![base_path.clone()], path.to_owned()) .await .expect("Copy operation should have succeeded"); assert!(base_path.exists(), "Original file should still exist"); assert!( path.join(format!("{base_name} ({} {i})", fl!("copy_noun"))) .exists(), "File should have been duplicated (copy #{i})" ); } Ok(()) } #[test(compio::test)] async fn copy_to_diff_dir_doesnt_dupe_files() -> io::Result<()> { let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); let (first_dir, second_dir) = { let mut dirs = filter_dirs(path)?; ( dirs.next().expect("Should have at least two dirs"), dirs.next().expect("Should have at least two dirs"), ) }; let first_file = filter_files(&first_dir)? .next() .expect("Should have at least one file"); // Both directories have a file with the same name. let base_name = first_file .file_name() .and_then(|name| name.to_str()) .expect("File name exists and is valid"); debug!( "Copying {} to {}", first_file.display(), second_dir.display() ); operation_copy(vec![first_file.clone()], second_dir.clone()) .await .expect(concat!( "Copy operation should have been cancelled ", "because we're copying to different directories ", "without replacement" )); assert!( first_dir.join(base_name).exists(), "First file should still exist" ); assert!( second_dir.join(base_name).exists(), "Second file should still exist" ); Ok(()) } #[test(compio::test)] async fn copy_file_with_diff_name_to_diff_dir() -> io::Result<()> { let fs = empty_fs()?; let path = fs.path(); let dir_path = path.join("cosmic"); fs::create_dir(&dir_path)?; let file_path = path.join("ferris"); File::create(&file_path)?; let expected = dir_path.join("ferris"); debug!("Copying {} to {}", file_path.display(), expected.display()); operation_copy(vec![file_path.clone()], dir_path.clone()) .await .expect("Copy operation should have succeeded"); assert!(file_path.exists(), "Original file should still exist"); assert!(expected.exists(), "File should have been copied"); Ok(()) } } ================================================ FILE: src/operation/notifiers.rs ================================================ // Copyright 2026 System76 // 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, notify: Arc, } static ACTIVELY_WRITING: LazyLock> = 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) } ================================================ FILE: src/operation/reader.rs ================================================ use std::path::Path; use std::{fs, io}; use crate::operation::OperationError; use super::Controller; // Special reader just for operations, handling cancel and progress pub struct OpReader { file: fs::File, metadata: fs::Metadata, current: u64, controller: Controller, } impl OpReader { pub fn new>(path: P, controller: Controller) -> io::Result { let file = fs::File::open(&path)?; let metadata = file.metadata()?; Ok(Self { file, metadata, current: 0, controller, }) } } impl io::Read for OpReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { cosmic::iced::futures::executor::block_on(async { self.controller .check() .await .map_err(|s| io::Error::other(OperationError::from_state(s, &self.controller))) })?; let count = self.file.read(buf)?; self.current += count as u64; let progress = self.current as f32 / self.metadata.len() as f32; self.controller.set_progress(progress); Ok(count) } } ================================================ FILE: src/operation/recursive.rs ================================================ // Copyright 2023 System76 // 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; 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, Move { cross_device_copy: bool }, } pub struct Context { buf: Vec, controller: Controller, on_progress: Box, on_replace: Pin>, pub(crate) op_sel: OperationSelection, replace_result_opt: Option, remaining_conflicts: usize, } pub trait OnProgress: Fn(&Op, &Progress) + 'static {} impl OnProgress for F where F: Fn(&Op, &Progress) + 'static {} pub trait OnReplace: for<'a> Fn(&'a Op, usize) -> Pin + 'a>> + 'static { } impl OnReplace for F where F: for<'a> Fn(&'a Op, usize) -> Pin + 'a>> + 'static { } impl Context { pub fn new(controller: Controller) -> Self { Self { // 128K is the optimal upper size of a buffer. buf: vec![0u8; 128 * 1024], controller, on_progress: Box::new(|_op, _progress| {}), on_replace: Box::pin(|_op, _count| Box::pin(async { ReplaceResult::Cancel })), op_sel: OperationSelection::default(), replace_result_opt: None, remaining_conflicts: 0, } } pub async fn recursive_copy_or_move( &mut self, from_to_pairs: impl IntoIterator, method: Method, ) -> Result { let mut ops = Vec::new(); let mut cleanup_ops = Vec::new(); let mut written_files = Vec::new(); let mut target_dirs = std::collections::HashSet::new(); for (from_parent, to_parent) in from_to_pairs { self.controller .check() .await .map_err(|s| OperationError::from_state(s, &self.controller))?; if from_parent == to_parent { // Skip matching source and destination continue; } for entry in WalkDir::new(&from_parent) { self.controller .check() .await .map_err(|s| OperationError::from_state(s, &self.controller))?; let entry = entry.map_err(|err| { OperationError::from_err( format!( "failed to walk directory {}: {}", from_parent.display(), err ), &self.controller, ) })?; let file_type = entry.file_type(); let from = entry.into_path(); let kind = if file_type.is_dir() { OpKind::Mkdir } else if file_type.is_file() { match method { Method::Copy => OpKind::Copy, Method::Move { cross_device_copy } => OpKind::Move { cross_device_copy }, } } else if file_type.is_symlink() { let target = fs::read_link(&from).map_err(|err| { OperationError::from_err( format!("failed to read link {}: {}", from_parent.display(), err), &self.controller, ) })?; OpKind::Symlink { target } } else { //TODO: present dialog and allow continue return Err(OperationError::from_err( format!("{} is not a known file type", from.display()), &self.controller, )); }; let to = if from == from_parent { // When copying a file, from matches from_parent, and to_parent must be used to_parent.clone() } else { let relative = from.strip_prefix(&from_parent).map_err(|err| { OperationError::from_err( format!( "failed to remove prefix {} from {}: {}", from_parent.display(), from.display(), err ), &self.controller, ) })?; //TODO: ensure to is inside of to_parent? to_parent.join(relative) }; let op = Op { kind, from, to, skipped: Rc::new(Skip { normal: Cell::new(false), cleanup: Cell::new(false), }), is_cleanup: false, }; if matches!(method, Method::Move { .. }) && let Some(cleanup_op) = op.move_cleanup_op() { cleanup_ops.push(cleanup_op); } if let Some(parent) = op.to.parent() { target_dirs.insert(parent.to_path_buf()); } ops.push(op); } self.op_sel.ignored.push(from_parent); } // Add cleanup ops after standard ops, in reverse cleanup_ops.reverse(); ops.append(&mut cleanup_ops); // Count potential conflicts (files that would need replacement) self.remaining_conflicts = ops .iter() .filter(|op| { matches!( op.kind, OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. } ) && op.to.is_file() }) .count(); let total_ops = ops.len(); for (current_ops, mut op) in ops.into_iter().enumerate() { self.controller .check() .await .map_err(|s| OperationError::from_state(s, &self.controller))?; let progress = Progress { current_ops, total_ops, current_bytes: 0, total_bytes: None, }; (self.on_progress)(&op, &progress); if op.run(self, progress).await.map_err(|err| { OperationError::from_err( format!( "failed to {:?} {} to {}: {}", op.kind, op.from.display(), op.to.display(), err ), &self.controller, ) })? { if matches!( op.kind, OpKind::Copy | OpKind::Move { cross_device_copy: true } ) { written_files.push(op.to.clone()); } // The from path is ignored in the operation selection if it is a top level item if self.op_sel.ignored.contains(&op.from) { // So add the to path to the selection self.op_sel.selected.push(op.to); } } else { // Cancelled return Ok(false); } } // Flush files to disk sync_to_disk(written_files, target_dirs).await; Ok(true) } pub fn on_progress(mut self, f: F) -> Self { self.on_progress = Box::new(f); self } pub fn on_replace(mut self, f: impl OnReplace + 'static) -> Self { self.on_replace = Box::pin(f); self } async fn replace(&mut self, op: &Op) -> Result, Box> { let replace_result = match self.replace_result_opt { Some(result) => result, None => (self.on_replace)(op, self.remaining_conflicts).await, }; match replace_result { ReplaceResult::Replace(apply_to_all) => { if apply_to_all { self.replace_result_opt = Some(replace_result); } compio::fs::remove_file(&op.to).await?; Ok(ControlFlow::Continue(op.to.clone())) } ReplaceResult::KeepBoth => match op.to.parent() { Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))), None => Err(format!("failed to get parent of {}", op.to.display()).into()), }, ReplaceResult::Skip(apply_to_all) => { if apply_to_all { self.replace_result_opt = Some(replace_result); } op.skipped.normal.set(true); Ok(ControlFlow::Break(true)) } ReplaceResult::Cancel => Ok(ControlFlow::Break(false)), } } } #[derive(Debug)] pub struct Progress { pub current_ops: usize, pub total_ops: usize, pub current_bytes: u64, pub total_bytes: Option, } #[derive(Debug)] pub enum OpKind { Copy, Move { cross_device_copy: bool }, Mkdir, Remove, Rmdir, Symlink { target: PathBuf }, } #[derive(Debug)] pub struct Skip { /// Normal operation should be skipped pub normal: Cell, /// Cleanup operation should be skipped pub cleanup: Cell, } #[derive(Debug)] pub struct Op { pub kind: OpKind, pub from: PathBuf, pub to: PathBuf, pub skipped: Rc, pub is_cleanup: bool, } impl Op { fn move_cleanup_op(&self) -> Option { let kind = match self.kind { OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. } => OpKind::Remove, OpKind::Mkdir => OpKind::Rmdir, OpKind::Remove | OpKind::Rmdir => return None, }; Some(Self { kind, from: self.from.clone(), //TODO: it is strange to have `to` here to: self.to.clone(), skipped: self.skipped.clone(), is_cleanup: true, }) } async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result> { if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) { return Ok(true); } match self.kind { OpKind::Copy => { crate::operation::actively_writing_add(self.to.clone()); let result = self.copy(ctx, progress).await; if result.is_err() { _ = compio::fs::remove_file(&self.to).await; } 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? { ControlFlow::Continue(to) => { self.to = to; } ControlFlow::Break(ret) => { return Ok(ret); } } } // This is atomic and ensures `to` is not created by any other process match compio::fs::hard_link(&self.from, &self.to).await { Ok(()) => {} Err(err) => { // https://docs.rs/windows-sys/latest/windows_sys/Win32/Foundation/constant.ERROR_NOT_SAME_DEVICE.html #[cfg(windows)] const EXDEV: i32 = 17; #[cfg(unix)] const EXDEV: i32 = libc::EXDEV as _; if err.raw_os_error() == Some(EXDEV) { if cross_device_copy { // Do not clean up if cross_device_copy is set self.skipped.cleanup.set(true); } // Try standard copy if hard link fails with cross device error let mut copy_op = Self { kind: OpKind::Copy, from: self.from.clone(), to: self.to.clone(), skipped: self.skipped.clone(), is_cleanup: self.is_cleanup, }; return Box::pin(copy_op.run(ctx, progress)).await; } return Err(err.into()); } } } OpKind::Mkdir => { compio::fs::create_dir_all(&self.to).await?; } OpKind::Remove => { compio::fs::remove_file(&self.from).await?; } OpKind::Rmdir => { compio::fs::remove_dir(&self.from).await?; } OpKind::Symlink { ref target } => { // 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); } } } #[cfg(unix)] { std::os::unix::fs::symlink(target, &self.to)?; } #[cfg(windows)] { if target.is_dir() { std::os::windows::fs::symlink_dir(target, &self.to)?; } else { std::os::windows::fs::symlink_file(target, &self.to)?; } } } } Ok(true) } async fn copy( &mut self, ctx: &mut Context, mut progress: Progress, ) -> Result> { // 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); } } } } } ================================================ FILE: src/spawn_detached.rs ================================================ use std::{io, process}; // This code is from the open crate and retains its MIT license. pub fn spawn_detached(command: &mut process::Command) -> io::Result<()> { command .stdin(process::Stdio::null()) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()); #[cfg(unix)] unsafe { use std::os::unix::process::CommandExt as _; command .pre_exec(move || { match libc::fork() { -1 => return Err(io::Error::last_os_error()), 0 => (), _ => libc::_exit(0), } if libc::setsid() == -1 { return Err(io::Error::last_os_error()); } Ok(()) }) .spawn()? .wait() .map(|_| ()) } #[cfg(windows)] { use std::os::windows::process::CommandExt; const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const CREATE_NO_WINDOW: u32 = 0x08000000; command .creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) .spawn() .map(|_| ()) } } ================================================ FILE: src/tab.rs ================================================ #[cfg(feature = "desktop")] use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env}; use cosmic::iced::advanced::graphics; use cosmic::iced::advanced::text::{self, Paragraph}; use cosmic::iced::alignment::Vertical; use cosmic::iced::clipboard::dnd::DndAction; use cosmic::iced::core::mouse::ScrollDelta; use cosmic::iced::core::widget::tree; use cosmic::iced::futures::{self, SinkExt}; use cosmic::iced::keyboard::Modifiers; use cosmic::iced::widget::scrollable::{self, AbsoluteOffset, Viewport}; use cosmic::iced::widget::{rule, stack}; use cosmic::iced::{ Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector, padding, stream, window, }; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space}; use cosmic::{Apply, Element, cosmic_theme, font, theme}; use i18n_embed::LanguageLoader; use icu::datetime::input::DateTime; use icu::datetime::options::TimePrecision; use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets}; use icu::locale::preferences::extensions::unicode::keywords::HourCycle; use image::{DynamicImage, ImageReader}; use jiff_icu::ConvertFrom; use mime_guess::{Mime, mime}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::cell::Cell; use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::error::Error; use std::fmt::{self, Display}; use std::fs::{self, File, Metadata}; use std::hash::Hash; use std::io::{BufRead, BufReader}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::path::{self, Path, PathBuf}; use std::sync::{Arc, LazyLock, RwLock, atomic}; use std::time::{Duration, Instant, SystemTime}; use tempfile::NamedTempFile; use tokio::sync::mpsc; use trash::{TrashItem, TrashItemMetadata, TrashItemSize}; use walkdir::WalkDir; use crate::app::{Action, PreviewItem, PreviewKind}; use crate::clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}; use crate::config::{ ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg, }; use crate::dialog::DialogKind; use crate::large_image::{ LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker, should_use_tiling, }; use crate::localize::{LANGUAGE_SORTER, LOCALE}; use crate::mime_icon::{mime_for_path, mime_icon}; use crate::mounter::MOUNTERS; use crate::operation::{Controller, OperationError}; use crate::thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize}; use crate::thumbnailer::thumbnailer; use crate::trash::{Trash, TrashExt}; use crate::{FxOrderMap, fl, menu, mime_app, mouse_area}; pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); pub const HOVER_DURATION: Duration = Duration::from_millis(1600); pub const TYPE_SELECT_TIMEOUT: Duration = Duration::from_millis(1000); //TODO: best limit for search items const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20); const MAX_SEARCH_RESULTS: usize = 200; //TODO: configurable thumbnail size? const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32); // Thumbnail generation semaphore - limits parallel thumbnail workers // Uses 4 workers for balanced throughput and memory usage pub static THUMB_SEMAPHORE: LazyLock = LazyLock::new(|| tokio::sync::Semaphore::const_new(num_cpus::get().min(4))); pub(crate) static SORT_OPTION_FALLBACK: LazyLock> = LazyLock::new(|| { FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| { ( Location::Path(dir).normalize().to_string(), (HeadingOptions::Modified, false), ) })) }); static MODE_NAMES: LazyLock> = LazyLock::new(|| { vec![ // Mode 0 fl!("none"), // Mode 1 fl!("execute-only"), // Mode 2 fl!("write-only"), // Mode 3 fl!("write-execute"), // Mode 4 fl!("read-only"), // Mode 5 fl!("read-execute"), // Mode 6 fl!("read-write"), // Mode 7 fl!("read-write-execute"), ] }); static SPECIAL_DIRS: LazyLock> = LazyLock::new(|| { let mut special_dirs = FxHashMap::default(); if let Some(dir) = dirs::document_dir() { special_dirs.insert(dir, "folder-documents"); } if let Some(dir) = dirs::download_dir() { special_dirs.insert(dir, "folder-download"); } if let Some(dir) = dirs::audio_dir() { special_dirs.insert(dir, "folder-music"); } if let Some(dir) = dirs::picture_dir() { special_dirs.insert(dir, "folder-pictures"); } if let Some(dir) = dirs::public_dir() { special_dirs.insert(dir, "folder-publicshare"); } if let Some(dir) = dirs::template_dir() { special_dirs.insert(dir, "folder-templates"); } if let Some(dir) = dirs::video_dir() { special_dirs.insert(dir, "folder-videos"); } if let Some(dir) = dirs::desktop_dir() { special_dirs.insert(dir, "user-desktop"); } if let Some(dir) = dirs::home_dir() { special_dirs.insert(dir, "user-home"); } special_dirs }); fn button_appearance( theme: &theme::Theme, selected: bool, highlighted: bool, cut: bool, focused: bool, accent: bool, condensed_radius: bool, desktop: bool, ) -> widget::button::Style { let cosmic = theme.cosmic(); let mut appearance = widget::button::Style::new(); if selected { if accent { appearance.background = Some(Color::from(cosmic.accent_color()).into()); appearance.icon_color = Some(Color::from(cosmic.on_accent_color())); if cut { appearance.text_color = Some(Color::from(cosmic.accent.on_disabled)); } else { appearance.text_color = Some(Color::from(cosmic.on_accent_color())); } } else { appearance.background = Some(Color::from(cosmic.bg_component_color()).into()); } } else if highlighted { if accent { appearance.background = Some(Color::from(cosmic.bg_component_color()).into()); appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color())); appearance.text_color = Some(Color::from(cosmic.on_bg_component_color())); if cut { appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled)); } else { appearance.text_color = Some(Color::from(cosmic.on_bg_component_color())); } } else { appearance.background = Some(Color::from(cosmic.bg_component_color()).into()); } } else if desktop { appearance.background = Some(Color::from(cosmic.bg_color()).into()); appearance.icon_color = Some(Color::from(cosmic.on_bg_color())); if cut { appearance.text_color = Some(Color::from(cosmic.background.component.disabled)); } else { appearance.text_color = Some(Color::from(cosmic.on_bg_color())); } } else if cut { appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled)); } if focused && accent { appearance.outline_width = 1.0; appearance.outline_color = Color::from(cosmic.accent_color()); appearance.border_width = 2.0; appearance.border_color = Color::TRANSPARENT; } if condensed_radius { appearance.border_radius = cosmic.radius_xs().into(); } else { appearance.border_radius = cosmic.radius_s().into(); } appearance } fn button_style( selected: bool, highlighted: bool, cut: bool, accent: bool, condensed_radius: bool, desktop: bool, ) -> theme::Button { //TODO: move to libcosmic? theme::Button::Custom { active: Box::new(move |focused, theme| { button_appearance( theme, selected, highlighted, cut, focused, accent, condensed_radius, desktop, ) }), disabled: Box::new(move |theme| { button_appearance( theme, selected, highlighted, cut, false, accent, condensed_radius, desktop, ) }), hovered: Box::new(move |focused, theme| { button_appearance( theme, selected, highlighted, cut, focused, accent, condensed_radius, desktop, ) }), pressed: Box::new(move |focused, theme| { button_appearance( theme, selected, highlighted, cut, focused, accent, condensed_radius, desktop, ) }), } } pub fn folder_icon(path: &PathBuf, icon_size: u16) -> widget::icon::Handle { widget::icon::from_name(SPECIAL_DIRS.get(path).map_or("folder", |x| *x)) .size(icon_size) .handle() } pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Handle { widget::icon::from_name(format!( "{}-symbolic", SPECIAL_DIRS.get(path).map_or("folder", |x| *x) )) .size(icon_size) .handle() } //TODO: replace with Path::has_trailing_sep when stable fn has_trailing_sep(path: &Path) -> bool { path.as_os_str() .as_encoded_bytes() .last() .copied() .is_some_and(|b| path::is_separator(b as char)) } fn tab_complete(path: &Path) -> Result, Box> { let parent = if has_trailing_sep(path) && path.is_dir() { // Show completions inside existing child directory instead of parent path } else { path.parent() .ok_or_else(|| format!("path has no parent {}", path.display()))? }; let child_os = path.strip_prefix(parent)?; let child = child_os .to_str() .ok_or_else(|| format!("invalid UTF-8 {}", child_os.display()))?; let pattern = format!("^{}", regex::escape(child)); let regex = regex::RegexBuilder::new(&pattern) .case_insensitive(true) .build()?; let mut completions = Vec::new(); for entry_res in fs::read_dir(parent)? { let entry = entry_res?; let file_name_os = entry.file_name(); let Some(file_name) = file_name_os.to_str() else { continue; }; // Don't list hidden files before entering a pattern if pattern == "^" && file_name.starts_with('.') { continue; } if regex.is_match(file_name) { completions.push((file_name.to_string(), entry.path())); } } completions.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.0, &b.0)); //TODO: make the list scrollable? completions.truncate(8); Ok(completions) } //TODO: translate, add more levels? fn format_size(size: u64) -> String { const KB: u64 = 1000; const MB: u64 = 1000 * KB; const GB: u64 = 1000 * MB; const TB: u64 = 1000 * GB; if size >= TB { format!("{:.1} TB", size as f64 / TB as f64) } else if size >= GB { format!("{:.1} GB", size as f64 / GB as f64) } else if size >= MB { format!("{:.1} MB", size as f64 / MB as f64) } else if size >= KB { format!("{:.1} KB", size as f64 / KB as f64) } else { format!("{size} B") } } const MODE_SHIFT_USER: u32 = 6; const MODE_SHIFT_GROUP: u32 = 3; const MODE_SHIFT_OTHER: u32 = 0; const fn get_mode_part(mode: u32, shift: u32) -> u32 { (mode >> shift) & 0o7 } fn set_mode_part(mode: u32, shift: u32, bits: u32) -> u32 { assert!(bits <= 0o7); (mode & !(0o7 << shift)) | (bits << shift) } fn date_time_formatter(military_time: bool) -> DateTimeFormatter { let mut prefs = DateTimeFormatterPreferences::from(LOCALE.clone()); prefs.hour_cycle = Some(if military_time { HourCycle::H23 } else { HourCycle::H12 }); let mut fs = fieldsets::YMDT::medium(); fs = fs.with_time_precision(TimePrecision::Minute); DateTimeFormatter::try_new(prefs, fs).expect("failed to create DateTimeFormatter") } fn time_formatter(military_time: bool) -> DateTimeFormatter { let mut prefs = DateTimeFormatterPreferences::from(LOCALE.clone()); prefs.hour_cycle = Some(if military_time { HourCycle::H23 } else { HourCycle::H12 }); let mut fs = fieldsets::T::medium(); fs = fs.with_time_precision(TimePrecision::Minute); DateTimeFormatter::try_new(prefs, fs).expect("failed to create DateTimeFormatter") } struct FormatTime<'a> { pub time: SystemTime, pub date_time_formatter: &'a DateTimeFormatter, pub time_formatter: &'a DateTimeFormatter, } impl<'a> FormatTime<'a> { fn from_secs( secs: i64, date_time_formatter: &'a DateTimeFormatter, time_formatter: &'a DateTimeFormatter, ) -> Option { // This looks convoluted because we need to ensure the units match up let secs: u64 = secs.try_into().ok()?; let now = SystemTime::now(); let filetime_diff = now .duration_since(SystemTime::UNIX_EPOCH) .map(|from_epoch| from_epoch.as_secs()) .ok() .and_then(|now_secs| now_secs.checked_sub(secs)) .map(Duration::from_secs)?; now.checked_sub(filetime_diff).map(|time| Self { time, date_time_formatter, time_formatter, }) } } impl Display for FormatTime<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let zoned = jiff::Zoned::try_from(self.time).unwrap(); let now = jiff::Zoned::now(); let icu_datetime = DateTime::convert_from(zoned.datetime()); if zoned.date() == now.date() { f.write_str(fl!("today").as_str())?; f.write_str(", ")?; self.time_formatter.format(&icu_datetime).fmt(f) } else { self.date_time_formatter.format(&icu_datetime).fmt(f) } } } const fn format_time<'a>( time: SystemTime, date_time_formatter: &'a DateTimeFormatter, time_formatter: &'a DateTimeFormatter, ) -> FormatTime<'a> { FormatTime { time, date_time_formatter, time_formatter, } } #[cfg(not(target_os = "windows"))] fn hidden_attribute(_metadata: &Metadata) -> bool { false } #[cfg(target_os = "windows")] fn hidden_attribute(metadata: &Metadata) -> bool { use std::os::windows::fs::MetadataExt; // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants const FILE_ATTRIBUTE_HIDDEN: u32 = 2; metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FsKind { Local, Remote, Gvfs, } #[cfg(target_os = "linux")] pub fn fs_kind(metadata: &Metadata) -> FsKind { //TODO: method to reload remote filesystems dynamically //TODO: fix for https://github.com/eminence/procfs/issues/262 static DEVICES: LazyLock> = LazyLock::new(|| { let mut devices = FxHashMap::default(); match procfs::process::Process::myself() { Ok(process) => match process.mountinfo() { Ok(mount_infos) => { devices = FxHashMap::from_iter(mount_infos.iter().filter_map(|mount_info| { let mut parts = mount_info.majmin.split(':'); let major_str = parts.next()?; let minor_str = parts.next()?; let major = major_str.parse::().ok()?; let minor = minor_str.parse::().ok()?; let dev = libc::makedev(major, minor); // Network and distributed filesystem types // Based on common remote filesystem types found in /proc/mounts let kind = match mount_info.fs_type.as_str() { // SMB/CIFS variants "cifs" | "smb" | "smb2" | "smbfs" => FsKind::Remote, // NFS variants "nfs" | "nfs4" => FsKind::Remote, // FUSE-based remote filesystems "fuse.rclone" | "fuse.sshfs" | "fuse.davfs2" | "fuse.ceph" | "fuse.glusterfs" | "fuse.s3fs" | "fuse.goofys" | "fuse.gcsfuse" | "fuse.afp" | "fuse.afpfs" => FsKind::Remote, // Other network protocols "afs" | "coda" | "ncpfs" | "davfs" | "davfs2" | "shfs" => { FsKind::Remote } // Cluster/distributed filesystems "ceph" | "glusterfs" | "lustre" | "gfs" | "gfs2" | "ocfs2" => { FsKind::Remote } // GVFS (GNOME Virtual File System) "fuse.gvfsd-fuse" => FsKind::Gvfs, // Everything else is local _ => FsKind::Local, }; Some((dev, kind)) })); } Err(err) => { log::warn!("failed to get mount info: {err}"); } }, Err(err) => { log::warn!("failed to get process info: {err}"); } } devices }); DEVICES.get(&metadata.dev()).map_or(FsKind::Local, |x| *x) } #[cfg(not(target_os = "linux"))] pub fn fs_kind(_metadata: &Metadata) -> FsKind { //TODO: support BSD, macOS, Windows? FsKind::Local } #[cfg(not(feature = "desktop"))] fn get_desktop_file_display_name(path: &Path) -> Option { None } #[cfg(feature = "desktop")] fn get_desktop_file_display_name(path: &Path) -> Option { let locales = get_languages_from_env(); let entry = match DesktopEntry::from_path(path, Some(&locales)) { Ok(ok) => ok, Err(err) => { log::warn!("failed to parse {}: {}", path.display(), err); return None; } }; entry.name(&locales).map(|s| s.into_owned()) } #[cfg(not(feature = "desktop"))] fn get_desktop_file_icon(path: &Path) -> Option { None } #[cfg(feature = "desktop")] fn get_desktop_file_icon(path: &Path) -> Option { let entry = match DesktopEntry::from_path::<&str>(path, None) { Ok(ok) => ok, Err(err) => { log::warn!("failed to parse {}: {}", path.display(), err); return None; } }; entry.icon().map(str::to_string) } /// Creates an icon handle from a desktop file's Icon field value. /// Supports both icon names (looked up in theme) and absolute paths (used directly). fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle { let icon_path = Path::new(icon); if icon_path.is_absolute() && icon_path.exists() { widget::icon::from_path(icon_path.to_path_buf()) } else { widget::icon::from_name(icon).size(size).handle() } } #[cfg(feature = "desktop")] pub fn parse_desktop_file(path: &Path) -> (Option, Option) { let locales = get_languages_from_env(); let entry = match DesktopEntry::from_path(path, Some(&locales)) { Ok(ok) => ok, Err(err) => { log::warn!("failed to parse {}: {}", path.display(), err); return (None, None); } }; ( entry.name(&locales).map(|s| s.into_owned()), entry.icon().map(str::to_string), ) } fn display_name_for_file(path: &Path, name: &str, get_from_gvfs: bool, is_desktop: bool) -> String { if is_desktop { return get_desktop_file_display_name(path).map_or_else( || Item::display_name(name), |desktop_name| Item::display_name(desktop_name.as_str()), ); } else if get_from_gvfs { #[cfg(feature = "gvfs")] { let file = gio::File::for_path(path); if let Ok(info) = gio::prelude::FileExt::query_info( &file, "standard::display-name", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE, ) { return Item::display_name(info.display_name().as_str()); } } } Item::display_name(name) } #[cfg(feature = "gvfs")] pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item { let file_name = file_info .attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_NAME) .unwrap_or_default(); let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let mut is_desktop = false; let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE); let is_dir = matches!(file_info.file_type(), gio::FileType::Directory); let size_opt = (!is_dir).then_some(file_info.size() as u64); let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if is_dir { ( //TODO: make this a static "inode/directory".parse().unwrap(), folder_icon(&path, sizes.grid()), folder_icon(&path, sizes.list()), folder_icon(&path, sizes.list_condensed()), ) } else { // ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive // @todo - expose this as a config option? let mime = mime_for_path(&path, None, true); //TODO: clean this up, implement for trash let icon_name_opt = if mime == "application/x-desktop" { is_desktop = true; get_desktop_file_icon(&path) } else { None }; if let Some(icon_name) = icon_name_opt { ( mime, desktop_icon_handle(&icon_name, sizes.grid()), desktop_icon_handle(&icon_name, sizes.list()), desktop_icon_handle(&icon_name, sizes.list_condensed()), ) } else { ( mime.clone(), mime_icon(mime.clone(), sizes.grid()), mime_icon(mime.clone(), sizes.list()), mime_icon(mime, sizes.list_condensed()), ) } }; let mut children_opt = None; let mut dir_size = DirSize::NotDirectory; if is_dir && !remote { dir_size = DirSize::Calculating(Controller::default()); //TODO: calculate children in the background (and make it cancellable?) match fs::read_dir(&path) { Ok(entries) => { children_opt = Some(entries.count()); } Err(err) => { log::warn!("failed to read directory {}: {}", path.display(), err); } } } let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop); let hidden = file_name.starts_with('.'); Item { name: file_name.into(), display_name, is_mount_point: false, metadata: ItemMetadata::GvfsPath { mtime, size_opt, children_opt, }, hidden, image_dimensions: (!remote && mime.type_() == mime::IMAGE) .then(|| image::image_dimensions(&path).ok()) .flatten(), location_opt: Some(Location::Path(path)), mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, thumbnail_opt: if remote { Some(ItemThumbnail::NotImage) } else { None }, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), selected: false, highlighted: false, overlaps_drag_rect: false, dir_size, cut: false, } } pub fn item_from_search_item(search_item: SearchItem, sizes: IconSizes) -> Item { match search_item { SearchItem::Path(path, name, metadata) => item_from_entry(path, name, metadata, sizes), SearchItem::Trash(entry, metadata) => item_from_trash_entry(entry, metadata, sizes), } } pub fn item_from_entry( path: PathBuf, name: String, metadata: fs::Metadata, sizes: IconSizes, ) -> Item { let mut is_desktop = false; let mut is_gvfs = false; let hidden = name.starts_with('.') || hidden_attribute(&metadata); let remote = match fs_kind(&metadata) { FsKind::Local => false, FsKind::Remote => true, #[cfg(feature = "gvfs")] FsKind::Gvfs => { is_gvfs = true; let file = gio::File::for_path(&path); match gio::prelude::FileExt::query_filesystem_info( &file, gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE, gio::Cancellable::NONE, ) { Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE), Err(err) => { log::warn!( "failed to get GIO filesystem info for {}: {}", path.display(), err ); true } } } #[cfg(not(feature = "gvfs"))] FsKind::Gvfs => { log::info!( "gvfs feature not enabled, info may be inaccurate for {}", path.display() ); true } }; let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if metadata.is_dir() { ( //TODO: make this a static "inode/directory".parse().unwrap(), folder_icon(&path, sizes.grid()), folder_icon(&path, sizes.list()), folder_icon(&path, sizes.list_condensed()), ) } else { let mime = mime_for_path(&path, Some(&metadata), remote); //TODO: clean this up, implement for trash let icon_name_opt = if mime == "application/x-desktop" { is_desktop = true; get_desktop_file_icon(&path) } else { None }; if let Some(icon_name) = icon_name_opt { ( mime, desktop_icon_handle(&icon_name, sizes.grid()), desktop_icon_handle(&icon_name, sizes.list()), desktop_icon_handle(&icon_name, sizes.list_condensed()), ) } else { ( mime.clone(), mime_icon(mime.clone(), sizes.grid()), mime_icon(mime.clone(), sizes.list()), mime_icon(mime, sizes.list_condensed()), ) } }; let mut children_opt = None; let mut dir_size = DirSize::NotDirectory; if metadata.is_dir() && !remote { dir_size = DirSize::Calculating(Controller::default()); //TODO: calculate children in the background (and make it cancellable?) match fs::read_dir(&path) { Ok(entries) => { children_opt = Some(entries.count()); } Err(err) => { log::warn!("failed to read directory {}: {}", path.display(), err); } } } let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop); Item { name, display_name, is_mount_point: false, metadata: ItemMetadata::Path { metadata, children_opt, }, hidden, location_opt: Some(Location::Path(path)), image_dimensions: None, mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, thumbnail_opt: remote.then_some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), selected: false, highlighted: false, overlaps_drag_rect: false, dir_size, cut: false, } } pub fn item_from_trash_entry( entry: TrashItem, metadata: TrashItemMetadata, sizes: IconSizes, ) -> Item { let original_path = entry.original_path(); let name = entry.name.to_string_lossy().into_owned(); let display_name = Item::display_name(&name); let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size { trash::TrashItemSize::Entries(_) => ( //TODO: make this a static "inode/directory".parse().unwrap(), folder_icon(&original_path, sizes.grid()), folder_icon(&original_path, sizes.list()), folder_icon(&original_path, sizes.list_condensed()), ), trash::TrashItemSize::Bytes(_) => { // This passes remote = true so it does not read from the original path let mime = mime_for_path(&original_path, None, true); ( mime.clone(), mime_icon(mime.clone(), sizes.grid()), mime_icon(mime.clone(), sizes.list()), mime_icon(mime, sizes.list_condensed()), ) } }; Item { name, display_name, is_mount_point: false, metadata: ItemMetadata::Trash { metadata, entry }, hidden: false, location_opt: None, image_dimensions: (mime.type_() == mime::IMAGE) .then(|| image::image_dimensions(&original_path).ok()) .flatten(), mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), selected: false, highlighted: false, overlaps_drag_rect: false, dir_size: DirSize::NotDirectory, cut: false, } } fn get_filename_from_path(path: &Path) -> Result { Ok(match path.file_name() { Some(name_os) => name_os .to_str() .ok_or_else(|| { format!( "failed to parse file name for {}: {name_os:?} is not valid UTF-8", path.display() ) })? .to_string(), None => fl!("filesystem"), }) } pub fn item_from_path>(path: P, sizes: IconSizes) -> Result { let path = path.into(); let name = get_filename_from_path(&path)?; let metadata = fs::metadata(&path) .map_err(|err| format!("failed to read metadata for {}: {}", path.display(), err))?; Ok(item_from_entry(path, name, metadata, sizes)) } pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { let mut items = Vec::new(); let mut hidden_files = Box::from([]); let mut remote_scannable = false; #[cfg(feature = "gvfs")] { if let Ok(path_meta) = fs::metadata(tab_path) && fs_kind(&path_meta) == FsKind::Gvfs { let file = gio::File::for_path(tab_path); // gio crate expects a comma delimited string let attr_string = [ gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(), gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(), gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(), gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(), gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(), gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(), ] .join(","); match gio::prelude::FileExt::enumerate_children( &file, attr_string.as_str(), gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE, ) { Ok(res) => { remote_scannable = true; items = res .filter_map(|file| { let file = file.ok()?; Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes)) }) .collect(); } Err(err) => { log::warn!( "could not enumerate {} via gio: {}", tab_path.display(), err ); } } } } if !remote_scannable { match fs::read_dir(tab_path) { Ok(entries) => { items = entries .filter_map(|entry_res| { let entry = entry_res .inspect_err(|err| { log::warn!( "failed to read entry in {}: {}", tab_path.display(), err ) }) .ok()?; let path = entry.path(); let name = entry .file_name() .into_string() .inspect_err(|name_os| { log::warn!( "failed to parse entry at {}: {:?} is not valid UTF-8", path.display(), name_os ) }) .ok()?; if name == ".hidden" && path.is_file() { hidden_files = parse_hidden_file(&path); } let metadata = fs::metadata(&path) .inspect_err(|err| { log::warn!( "failed to read metadata for entry at {}: {}", path.display(), err ) }) .ok()?; Some(item_from_entry(path, name, metadata, sizes)) }) .collect(); } Err(err) => { log::warn!("failed to read directory {}: {}", tab_path.display(), err); } } } items.sort_unstable_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), }); for item in &mut items { if hidden_files.contains(&item.name) { item.hidden = true; } } items } pub fn scan_search bool + Sync>( search_location: &SearchLocation, term: &str, show_hidden: bool, callback: F, ) { if term.is_empty() { return; } let pattern = regex::escape(term); let regex = match regex::RegexBuilder::new(&pattern) .case_insensitive(true) .build() { Ok(ok) => ok, Err(err) => { log::warn!("failed to parse regex {pattern:?}: {err}"); return; } }; match search_location { SearchLocation::Path(tab_path) => { ignore::WalkBuilder::new(tab_path) .standard_filters(false) .hidden(!show_hidden) //TODO: only use this on supported targets .same_file_system(true) .build_parallel() .run(|| { Box::new(|entry_res| { let Ok(entry) = entry_res else { // Skip invalid entries return ignore::WalkState::Skip; }; let Some(file_name) = entry.file_name().to_str() else { // Skip anything with an invalid name return ignore::WalkState::Skip; }; if regex.is_match(file_name) { let path = entry.path(); let metadata = match entry.metadata() { Ok(ok) => ok, Err(err) => { log::warn!( "failed to read metadata for entry at {}: {}", path.display(), err ); return ignore::WalkState::Continue; } }; if !callback(SearchItem::Path( path.to_path_buf(), file_name.to_string(), metadata, )) { return ignore::WalkState::Quit; } } ignore::WalkState::Continue }) }); } SearchLocation::Recents => { let recent_files = match recently_used_xbel::parse_file() { Ok(recent_files) => recent_files, Err(err) => { log::warn!("Error reading recent files: {err:?}"); return; } }; for bookmark in recent_files.bookmarks { let path = uri_to_path(bookmark.href); if let Some(path) = path && path.exists() { let file_name = path.file_name(); if let Some(file_name) = file_name { let file_name = file_name.to_string_lossy(); if regex.is_match(&file_name) { match path.metadata() { Ok(metadata) => { if !callback(SearchItem::Path( path.to_path_buf(), file_name.to_string(), metadata, )) { break; } } Err(err) => { log::warn!( "failed to read metadata for entry at {}: {}", path.display(), err ); } }; } } } } } SearchLocation::Trash => { Trash::scan_search(callback, ®ex); } } } fn uri_to_path(uri: String) -> Option { uri.parse::().ok().and_then(|url| { //TODO support for external drive or cloud? if url.scheme() == "file" { url.to_file_path().ok() } else { None } }) } pub fn has_recents() -> bool { match recently_used_xbel::parse_file() { Ok(recent_files) => !recent_files.bookmarks.is_empty(), Err(_) => false, } } pub fn scan_recents(sizes: IconSizes) -> Vec { let recent_files = match recently_used_xbel::parse_file() { Ok(recent_files) => recent_files, Err(err) => { log::warn!("Error reading recent files: {err:?}"); return Vec::new(); } }; let mut recents: Vec<_> = recent_files .bookmarks .into_iter() .filter_map(|bookmark| { let path = uri_to_path(bookmark.href)?; let last_edit = bookmark.modified.parse::().ok()?; let last_visit = bookmark.visited.parse::().ok()?; if path.exists() { let file_name = path.file_name()?; let name = file_name.to_string_lossy().to_string(); let metadata = match path.metadata() { Ok(ok) => ok, Err(err) => { log::warn!( "failed to read metadata for entry at {}: {}", path.display(), err ); return None; } }; let item = item_from_entry(path, name, metadata, sizes); Some((item, last_edit.min(last_visit))) } else { log::warn!("recent file path does not exist: {}", path.display()); None } }) .collect(); recents.sort_by_key(|recent| Reverse(recent.1)); recents.into_iter().take(50).map(|(item, _)| item).collect() } pub fn scan_network(uri: &str, sizes: IconSizes) -> Vec { for mounter in MOUNTERS.values() { match mounter.network_scan(uri, sizes) { Some(Ok(items)) => return items, Some(Err(err)) => { log::warn!("failed to scan {uri:?}: {err}"); } None => {} } } Vec::new() } //TODO: organize desktop items based on display pub fn scan_desktop( tab_path: &PathBuf, _display: &str, desktop_config: DesktopConfig, mut sizes: IconSizes, ) -> Vec { sizes.grid = desktop_config.icon_size; let mut items = Vec::new(); if desktop_config.show_content { items.extend(scan_path(tab_path, sizes)); } if desktop_config.show_mounted_drives { for mounter in MOUNTERS.values() { let Some(mounter_items) = mounter.items(sizes) else { continue; }; items.extend(mounter_items.into_iter().filter_map(|mounter_item| { let path = mounter_item.path()?; // Get most item data from path let mut item = match item_from_path(&path, sizes) { Ok(item) => item, Err(err) => { log::warn!( "failed to get item from mounter item {}: {}", path.display(), err ); return None; } }; //Override some data with mounter information item.name = mounter_item.name(); item.display_name = Item::display_name(&item.name); //TODO: use icon size for mounter item icon if let Some(icon) = mounter_item.icon(false) { item.icon_handle_grid.clone_from(&icon); item.icon_handle_list.clone_from(&icon); item.icon_handle_list_condensed = icon; } Some(item) })); } } if desktop_config.show_trash { let name = fl!("trash"); let display_name = Item::display_name(&name); let metadata = ItemMetadata::SimpleDir { entries: Trash::entries() as u64, }; let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { ( "inode/directory".parse().unwrap(), Trash::icon(sizes.grid()), Trash::icon(sizes.list()), Trash::icon(sizes.list_condensed()), ) }; items.push(Item { name, display_name, is_mount_point: false, metadata, hidden: false, location_opt: Some(Location::Trash), image_dimensions: None, mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), selected: false, highlighted: false, overlaps_drag_rect: false, dir_size: DirSize::NotDirectory, cut: false, }); } items } #[derive(Clone, Debug)] pub struct EditLocation { pub location: Location, pub completions: Option>, pub selected: Option, } impl EditLocation { pub fn resolve(&self) -> Option { if let Location::Network(uri, ..) = &self.location { MOUNTERS .values() .find_map(|mounter| mounter.dir_info(uri)) .map(|(uri, display_name, path_opt)| Location::Network(uri, display_name, path_opt)) } else { let Some(selected) = self.selected else { return Some(self.location.clone()); }; let completions = self.completions.as_ref()?; let completion = completions.get(selected)?; Some(self.location.with_path(completion.1.clone()).normalize()) } } pub fn select(&mut self, forwards: bool) { if let Some(completions) = &self.completions { if completions.is_empty() { self.selected = None; } else { let mut selected = if forwards { self.selected.and_then(|x| x.checked_add(1)).unwrap_or(0) } else { self.selected .and_then(|x| x.checked_sub(1)) .unwrap_or(completions.len() - 1) }; if selected >= completions.len() { selected = 0; } self.selected = Some(selected); // Automatically resolve if there is only one completion if completions.len() == 1 && let Some(resolved) = self.resolve() { self.location = resolved; self.selected = None; } } } else { self.selected = None; } } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum SearchLocation { Path(PathBuf), Recents, Trash, } impl std::fmt::Display for SearchLocation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Path(path) => write!(f, "{}", path.display()), Self::Recents => write!(f, "recents"), Self::Trash => write!(f, "trash"), } } } #[derive(Clone, Debug)] pub enum SearchItem { Path(PathBuf, String, fs::Metadata), Trash(TrashItem, TrashItemMetadata), } impl From for EditLocation { fn from(location: Location) -> Self { Self { location, completions: None, selected: None, } } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum Location { Desktop(PathBuf, String, DesktopConfig), Network(String, String, Option), Path(PathBuf), Recents, Search(SearchLocation, String, bool, Instant), Trash, } impl std::fmt::Display for Location { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Desktop(path, display, ..) => { write!(f, "{} on display {display}", path.display()) } Self::Network(uri, ..) => write!(f, "{uri}"), Self::Path(path) => write!(f, "{}", path.display()), Self::Recents => write!(f, "recents"), Self::Search(location, term, ..) => { write!(f, "search {} for {}", location, term) } Self::Trash => write!(f, "trash"), } } } impl Location { pub fn normalize(&self) -> Self { if let Location::Network(uri, ..) = self { if !uri.ends_with('/') { let mut uri = uri.clone(); uri.push('/'); self.with_uri(uri) } else { self.clone() } } else if let Some(mut path) = self.path_opt().cloned() { // Canonicalize path, if possible if let Ok(canonical) = fs::canonicalize(&path) { path = canonical; } // Add trailing slash if location is a directory if path.is_dir() { path.push(""); } self.with_path(path) } else { self.clone() } } pub fn ancestors(&self) -> Vec<(Self, String)> { self.path_opt().map_or_else(Default::default, |path| { path.ancestors() .scan(false, |found_home, ancestor| { (!*found_home).then(|| { let (name, is_home) = folder_name(ancestor); *found_home = is_home; (self.with_path(ancestor.to_path_buf()), name) }) }) .collect() }) } pub const fn path_opt(&self) -> Option<&PathBuf> { match self { Self::Desktop(path, ..) => Some(path), Self::Path(path) => Some(path), Self::Search(SearchLocation::Path(path), ..) => Some(path), Self::Network(_, _, path) => path.as_ref(), _ => None, } } pub(crate) fn into_path_opt(self) -> Option { match self { Self::Desktop(path, ..) => Some(path), Self::Path(path) => Some(path), Self::Search(SearchLocation::Path(path), ..) => Some(path), Self::Network(_, _, path) => path, _ => None, } } pub fn with_path(&self, path: PathBuf) -> Self { let path = Self::expand_tilde(path); match self { Self::Desktop(_, display, desktop_config) => { Self::Desktop(path, display.clone(), *desktop_config) } Self::Path(..) => Self::Path(path), Self::Search(SearchLocation::Path(_), term, show_hidden, time) => Self::Search( SearchLocation::Path(path), term.clone(), *show_hidden, *time, ), other => other.clone(), } } pub fn with_uri(&self, uri: String) -> Self { if let Self::Network(_, name, path) = self { Self::Network(uri, name.clone(), path.clone()) } else { self.clone() } } pub fn scan(&self, sizes: IconSizes) -> (Option>, Vec) { let items = match self { Self::Desktop(path, display, desktop_config) => { scan_desktop(path, display, *desktop_config, sizes) } Self::Path(path) => scan_path(path, sizes), Self::Search(..) => { // Search is done incrementally Vec::new() } Self::Trash => Trash::scan(sizes), Self::Recents => scan_recents(sizes), Self::Network(uri, _, _) => scan_network(uri, sizes), }; let parent_item_opt = match self.path_opt() { Some(path) => match item_from_path(path, sizes) { Ok(item) => Some(Box::new(item)), Err(err) => { log::warn!("failed to get item for {}: {}", path.display(), err); None } }, //TODO: support other locations? None => None, }; (parent_item_opt, items) } pub fn title(&self) -> String { match self { Self::Desktop(path, _, _) => { let (name, _) = folder_name(path); name } Self::Path(path) => { let (name, _) = folder_name(path); name } Self::Search(location, term, ..) => { let name = match location { SearchLocation::Path(path) => folder_name(path).0, SearchLocation::Trash => fl!("trash"), SearchLocation::Recents => fl!("recents"), }; //TODO: translate format!("Search \"{term}\": {name}") } Self::Trash => { fl!("trash") } Self::Recents => { fl!("recents") } Self::Network(display_name, ..) => display_name.clone(), } } /// Expand a path that starts with "~" with the /// user's home directory pub fn expand_tilde(path: PathBuf) -> PathBuf { let mut components = path.components(); match components.next() { Some(path::Component::Normal(os_str)) if os_str == "~" => { if let Some(home) = dirs::home_dir() { home.join(components.as_path()) } else { path } } _ => path, } } pub fn is_trash(&self) -> bool { matches!( self, Location::Trash | Location::Search(SearchLocation::Trash, ..) ) } pub fn is_recents(&self) -> bool { matches!( self, Location::Recents | Location::Search(SearchLocation::Recents, ..) ) } /// Returns true if this location supports paste operations (not Trash) pub fn supports_paste(&self) -> bool { matches!( self, Self::Desktop(..) | Self::Path(..) | Self::Search(..) | Self::Recents | Self::Network(_, _, Some(_)) ) } } pub struct TaskWrapper(pub cosmic::Task); impl From> for TaskWrapper { fn from(task: cosmic::Task) -> Self { Self(task) } } impl fmt::Debug for TaskWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TaskWrapper").finish() } } #[derive(Debug)] pub enum Command { Action(Action), AddNetworkDrive, AddToSidebar(PathBuf), AutoScroll(Option), ChangeLocation(String, Location, Option>), ContextMenu(Option, Option), Delete(Vec), DropFiles(PathBuf, ClipboardPaste), EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(cosmic::desktop::DesktopEntryData, usize), Iced(TaskWrapper), OpenFile(Vec), OpenInNewTab(PathBuf), OpenInNewWindow(PathBuf), OpenTrash, Preview(PreviewKind), RunContextAction(usize), SetOpenWith(Mime, String), SetPermissions(PathBuf, u32), SetMultiplePermissions(Vec<(PathBuf, u32)>), SetSort(String, HeadingOptions, bool), WindowDrag, WindowToggleMaximize, } #[derive(Clone, Debug)] pub enum Message { AddNetworkDrive, AutoScroll(Option), Click(Option), DoubleClick(Option), ClickRelease(Option), Config(TabConfig), ContextAction(Action), ContextMenu(Option, Option), LocationContextMenuPoint(Option), LocationContextMenuIndex(Option, Option), LocationMenuAction(LocationMenuAction), Drag(Option), DragEnd, EditLocation(Option), EditLocationComplete(usize), EditLocationEnable, EditLocationSubmit, EditLocationTab, OpenInNewTab(PathBuf), EmptyTrash, #[cfg(feature = "desktop")] ExecEntryAction(Option, usize), Gallery(bool), GalleryPrevious, GalleryNext, GalleryToggle, GoNext, GoPrevious, ItemDown, ItemLeft, ItemRight, ItemUp, Location(Location), LocationUp, Open(Option), Reload, RightClick(Option, Option), MiddleClick(usize), Resize(Rectangle), Scroll(Viewport), ScrollTab(f32), ScrollToFocused, SearchContext(Location, SearchContextWrapper), SearchReady(bool), SelectAll, SelectFirst, SelectLast, SetOpenWith(Mime, String), RunContextAction(usize), SetPermissions(PathBuf, u32), ShiftPermissions(Option<(PathBuf, u32)>, u32, u32), SetSort(HeadingOptions, bool), TabComplete(PathBuf, Vec<(String, PathBuf)>), Thumbnail(PathBuf, ItemThumbnail), ToggleSort(HeadingOptions), Drop(Option<(Location, ClipboardPaste)>), DndHover(Location), DndEnter(Location), DndLeave(Location), WindowDrag, WindowToggleMaximize, ZoomIn, ZoomOut, HighlightDeactivate(usize), HighlightActivate(usize), DirectorySize(PathBuf, DirSize), ImageDecoded(PathBuf, u32, u32, Vec, Option<(u32, u32)>, u64), // path, width, height, pixels, display_size, generation } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum LocationMenuAction { OpenInNewTab(usize), OpenInNewWindow(usize), Preview(usize), AddToSidebar(usize), } impl MenuAction for LocationMenuAction { type Message = Message; fn message(&self) -> Self::Message { Message::LocationMenuAction(*self) } } #[derive(Clone, Debug)] pub enum DirSize { Calculating(Controller), Directory(u64), NotDirectory, Error(String), } #[derive(Clone, Debug)] pub enum ItemMetadata { Path { metadata: Metadata, children_opt: Option, }, Trash { metadata: trash::TrashItemMetadata, entry: trash::TrashItem, }, SimpleDir { entries: u64, }, SimpleFile { size: u64, }, #[cfg(feature = "gvfs")] GvfsPath { mtime: u64, size_opt: Option, children_opt: Option, }, } impl ItemMetadata { pub fn is_dir(&self) -> bool { match self { Self::Path { metadata, .. } => metadata.is_dir(), Self::Trash { metadata, .. } => match metadata.size { trash::TrashItemSize::Entries(_) => true, trash::TrashItemSize::Bytes(_) => false, }, Self::SimpleDir { .. } => true, Self::SimpleFile { .. } => false, #[cfg(feature = "gvfs")] Self::GvfsPath { children_opt, .. } => children_opt.is_some(), } } pub fn modified(&self) -> Option { match self { Self::Path { metadata, .. } => metadata.modified().ok(), #[cfg(feature = "gvfs")] Self::GvfsPath { mtime, .. } => { Some(SystemTime::UNIX_EPOCH + Duration::from_secs(*mtime)) } _ => None, } } pub fn file_size(&self) -> Option { match self { Self::Path { metadata, .. } => (!metadata.is_dir()).then_some(metadata.len()), Self::Trash { metadata, .. } => match metadata.size { TrashItemSize::Bytes(size) => Some(size), TrashItemSize::Entries(_) => None, }, #[cfg(feature = "gvfs")] Self::GvfsPath { size_opt, .. } => *size_opt, _ => None, } } pub fn children_count(&self) -> Option<&usize> { match &self { ItemMetadata::Path { children_opt, .. } => children_opt.as_ref(), #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { children_opt, .. } => children_opt.as_ref(), _ => None, } } } #[derive(Debug)] pub enum ItemThumbnail { NotImage, Image(widget::image::Handle, Option<(u32, u32)>), Svg(widget::svg::Handle), Text(widget::text_editor::Content), } impl Clone for ItemThumbnail { fn clone(&self) -> Self { match self { Self::NotImage => Self::NotImage, Self::Image(handle, size_opt) => Self::Image(handle.clone(), *size_opt), Self::Svg(handle) => Self::Svg(handle.clone()), // Content cannot be cloned simply Self::Text(content) => { Self::Text(widget::text_editor::Content::with_text(&content.text())) } } } } impl ItemThumbnail { pub fn new( path: &Path, metadata: ItemMetadata, mime: mime::Mime, mut thumbnail_size: u32, max_mem: u64, jobs: usize, max_size_mb: u64, ) -> Self { let thumbnail_cacher = ThumbnailCacher::new(path, ThumbnailSize::from_pixel_size(thumbnail_size)); match thumbnail_cacher.as_ref() { Ok(cache) => match cache.get_cached_thumbnail() { CachedThumbnail::Valid((thumbnail_path, size)) => { // Check original image dimensions even when loading cached thumbnail // This prevents trying to load huge images in preview mode let original_dims = match image::image_dimensions(path) { Ok((width, height)) => Some((width, height)), Err(_) => size.map(|s| (s.pixel_size(), s.pixel_size())), }; return Self::Image( widget::image::Handle::from_path(thumbnail_path), original_dims, ); } CachedThumbnail::Failed => { if mime.type_() != mime::IMAGE { return Self::NotImage; } } CachedThumbnail::RequiresUpdate(size) => { thumbnail_size = size.pixel_size(); } }, Err(err) => { log::warn!( "failed to create ThumbnailCache for {}: {}", path.display(), err ); } } let size = metadata.file_size().unwrap_or_default(); let check_size = |thumbnailer: &str, max_size| { if size <= max_size { true } else { log::warn!( "skipping internal {} thumbnailer for {}: file size {} is larger than {}", thumbnailer, path.display(), format_size(size), format_size(max_size) ); false } }; let mut tried_supported_file = false; // First try built-in image thumbnailer if mime.type_() == mime::IMAGE && check_size("image", max_size_mb * 1000 * 1000) { // Check if image dimensions would exceed available memory budget // The GPU tiling system can handle large images, but we still need to decode them first let dimensions_ok = match image::image_dimensions(path) { Ok((width, height)) => { if exceeds_memory_limit(width, height, max_mem) { log::warn!( "skipping thumbnail generation for {}: {}x{} image would exceed {}MB memory budget", path.display(), width, height, max_mem ); false } else { if should_use_tiling(width, height) { log::info!( "Large image {}x{} detected, will use GPU tiling for display", width, height ); } true } } Err(err) => { log::debug!( "failed to read dimensions for {}: {}, will try decoding", path.display(), err ); true // If we can't read dimensions, try anyway } }; if !dimensions_ok { // Skip this image entirely since it is too large to safely decode return Self::NotImage; } tried_supported_file = true; let dyn_img = match image::ImageReader::open(path) .and_then(image::ImageReader::with_guessed_format) { Ok(mut reader) => { let mut limits = image::Limits::default(); let max_ram = max_mem * 1000 * 1000 / jobs as u64; limits.max_alloc = Some(max_ram); reader.limits(limits); match reader.decode() { Ok(reader) => Some(reader), Err(err) => { log::warn!("failed to decode {}: {}", path.display(), err); None } } } Err(err) => { log::warn!("failed to read {}: {}", path.display(), err); None } }; if let Some(dyn_img) = dyn_img { let (img_width, img_height) = (dyn_img.width(), dyn_img.height()); if let Ok(cacher) = thumbnail_cacher.as_ref() { match cacher.update_with_image(dyn_img) { Ok(thumb_path) => { return Self::Image( widget::image::Handle::from_path(thumb_path), Some((img_width, img_height)), ); } Err(err) => { log::warn!("cacher failed to decode {}: {}", path.display(), err); } } } else { // Fallback for when thumbnail cacher isn't available. let thumbnail = dyn_img .thumbnail(thumbnail_size, thumbnail_size) .into_rgba8(); return Self::Image( widget::image::Handle::from_rgba( thumbnail.width(), thumbnail.height(), thumbnail.into_raw(), ), Some((img_width, img_height)), ); } } } // Try external thumbnailers. let thumbnail_dir = thumbnail_cacher .as_ref() .ok() .map(ThumbnailCacher::thumbnail_dir); if let Some((item_thumbnail, temp_file)) = Self::generate_thumbnail_external(path, &mime, thumbnail_size, thumbnail_dir) { if let Ok(cache) = thumbnail_cacher && let Err(err) = cache.update_with_temp_file(temp_file) { log::warn!("failed to update cache for {}: {}", path.display(), err); } return item_thumbnail; } tried_supported_file = tried_supported_file || !thumbnailer(&mime).is_empty(); // Try internal thumbnailers that don't get cached. //TODO: adjust limits for internal thumbnailers as desired if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG && check_size("svg", 8 * 1000 * 1000) { tried_supported_file = true; // Try built-in svg thumbnailer match fs::read(path) { Ok(data) => { //TODO: validate SVG data return Self::Svg(widget::svg::Handle::from_memory(data)); } Err(err) => { log::warn!("failed to read {}: {}", path.display(), err); } } } else if mime.type_() == mime::TEXT && check_size("text", 8 * 1000 * 1000) { /*TODO: fix performance issues, widget::text_editor::Content::with_text forces all text to shape, which blocks rendering match fs::read_to_string(&path) { Ok(data) => { return ItemThumbnail::Text(widget::text_editor::Content::with_text(&data)); } Err(err) => { log::warn!("failed to read {}: {}", path.display(), err); } } */ } // If we weren't able to create a thumbnail, but we should have // been able to, create a fail marker so that it isn't tried the // next time. if let Ok(cacher) = thumbnail_cacher && tried_supported_file && let Err(err) = cacher.create_fail_marker() { log::warn!( "failed to create thumbnail fail marker for {}: {}", path.display(), err ); } Self::NotImage } fn generate_thumbnail_external( path: &Path, mime: &mime::Mime, thumbnail_size: u32, thumbnail_dir: Option<&Path>, ) -> Option<(Self, NamedTempFile)> { // Try external thumbnailers for thumbnailer in thumbnailer(mime) { let is_evince = thumbnailer.exec.starts_with("evince-thumbnailer "); let prefix = if is_evince { //TODO: apparmor config for evince-thumbnailer does not allow /tmp/cosmic-files* "gnome-desktop-" } else { "cosmic-files-" }; // It's preferable to create the tempfile in the same directory as the final cached // thumbnail to ensure that no copies across filesytems need to be made. However, // the apparmor config for evince-thumbnailer does not allow this, so we need to // fallback to the system tempdir. let dir = if is_evince { None } else { thumbnail_dir }; let file = match dir { Some(d) => tempfile::Builder::new().prefix(prefix).tempfile_in(d), None => tempfile::Builder::new().prefix(prefix).tempfile(), }; let file = match file { Ok(ok) => ok, Err(err) => { log::warn!( "failed to create temporary file for thumbnail of {}: {}", path.display(), err ); continue; } }; let Some(mut command) = thumbnailer.command(path, file.path(), thumbnail_size) else { continue; }; match command.status() { Ok(status) => { if status.success() { match image::ImageReader::open(file.path()) .and_then(ImageReader::with_guessed_format) { Ok(reader) => match reader.decode().map(DynamicImage::into_rgba8) { Ok(image) => { return Some(( Self::Image( widget::image::Handle::from_rgba( image.width(), image.height(), image.into_raw(), ), None, ), file, )); } Err(err) => { log::warn!("failed to decode {}: {}", path.display(), err); } }, Err(err) => { log::warn!("failed to read {}: {}", path.display(), err); } } } else { log::warn!( "failed to run {:?} for {}: {}", thumbnailer, path.display(), status ); } } Err(err) => { log::warn!( "failed to run {thumbnailer:?} for {}: {}", path.display(), err ); } } } None } } #[derive(Clone, Debug)] pub struct Item { pub name: String, pub is_mount_point: bool, pub display_name: String, pub metadata: ItemMetadata, pub hidden: bool, pub location_opt: Option, pub mime: Mime, pub image_dimensions: Option<(u32, u32)>, pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle, pub thumbnail_opt: Option, pub button_id: widget::Id, pub pos_opt: Cell>, pub rect_opt: Cell>, pub selected: bool, pub highlighted: bool, pub cut: bool, pub overlaps_drag_rect: bool, pub dir_size: DirSize, } impl Item { fn display_name(name: &str) -> String { // In order to wrap at periods and underscores, add a zero width space after each one name.replace('.', ".\u{200B}").replace('_', "_\u{200B}") } /// Text widget for a filename in grid/icon view: word-or-glyph wrapping, middle-ellipsized to 3 lines. fn grid_display_name<'a>( name: impl Into> + 'a, ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> { widget::text::body(name) .wrapping(text::Wrapping::WordOrGlyph) .ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines( 3, ))) } /// Text widget for a filename in list view: word-or-glyph wrapping, middle-ellipsized to 1 line. fn list_display_name<'a>( name: impl Into> + 'a, ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> { widget::text::body(name) .wrapping(text::Wrapping::WordOrGlyph) .ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines( 1, ))) } pub fn path_opt(&self) -> Option<&PathBuf> { self.location_opt.as_ref()?.path_opt() } pub fn can_gallery(&self) -> bool { self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT } pub fn file_metadata(&self) -> Option { match &self.metadata { ItemMetadata::Path { metadata, .. } => Some(metadata.clone()), #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { .. } => self.path_opt().and_then(|p| fs::metadata(p).ok()), _ => { //TODO: other metadata types None } } } fn preview(&self) -> Element<'_, Message> { let spacing = cosmic::theme::spacing(); // This loads the image only if thumbnailing worked let icon = widget::icon::icon(self.icon_handle_grid.clone()) .content_fit(ContentFit::Contain) .size(IconSizes::default().grid()) .into(); match self .thumbnail_opt .as_ref() .unwrap_or(&ItemThumbnail::NotImage) { ItemThumbnail::NotImage => icon, ItemThumbnail::Image(handle, _original_dims) => { // Preview pane: ALWAYS show thumbnail for instant, responsive UI // Full resolution loading happens in gallery mode widget::image(handle.clone()).into() } ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(), ItemThumbnail::Text(content) => widget::text_editor(content) .class(cosmic::theme::iced::TextEditor::Custom(Box::new( text_editor_class, ))) .width(THUMBNAIL_SIZE as f32) .height(Length::Fixed(THUMBNAIL_SIZE as f32)) .padding(spacing.space_xxs) .into(), } } pub fn preview_actions(&self) -> Element<'_, Message> { let mut row = widget::row::with_capacity(3) .align_y(Alignment::Center) .spacing(theme::spacing().space_xxs) .push( widget::button::icon(widget::icon::from_name("go-previous-symbolic")) .on_press(Message::ItemLeft), ) .push( widget::button::icon(widget::icon::from_name("go-next-symbolic")) .on_press(Message::ItemRight), ); if self.can_gallery() && let Some(_path) = self.path_opt() { row = row.push( widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) .on_press(Message::Gallery(true)), ); } row.into() } pub fn preview_view<'a>( &'a self, mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>, military_time: bool, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, space_m, .. } = theme::spacing(); let mut column = widget::column::with_capacity(4).spacing(space_m); column = column.push( widget::container(self.preview()) .center_x(Length::Fill) .max_height(THUMBNAIL_SIZE as f32), ); let mut details = widget::column::with_capacity(8).spacing(space_xxxs); details = details.push(widget::text::heading(self.name.clone())); details = details.push(widget::text::body(fl!( "type", mime = self.mime.to_string() ))); let mut settings = Vec::new(); if let Some(mime_app_cache) = mime_app_cache_opt { let mime_apps = mime_app_cache.get(&self.mime); if !mime_apps.is_empty() { settings.push( widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( mime_apps, mime_apps.iter().position(|x| x.is_default), move |index| index, ) .icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))), ) .map(|index| { let mime_app = &mime_apps[index]; Message::SetOpenWith(self.mime.clone(), mime_app.id.clone()) }), ), ); } } if let Some(metadata) = self.file_metadata() { if metadata.is_dir() { if let Some(children) = self.metadata.children_count() { details = details.push(widget::text::body(fl!("items", items = children))); } let size = match &self.dir_size { DirSize::Calculating(_) => fl!("calculating"), DirSize::Directory(size) => format_size(*size), DirSize::NotDirectory => String::new(), DirSize::Error(err) => err.clone(), }; if !size.is_empty() { details = details.push(widget::text::body(fl!("item-size", size = size))); } } else { details = details.push(widget::text::body(fl!( "item-size", size = format_size(metadata.len()) ))); } let date_time_formatter = date_time_formatter(military_time); let time_formatter = time_formatter(military_time); if let Ok(time) = metadata.created() { details = details.push(widget::text::body(fl!( "item-created", created = format_time(time, &date_time_formatter, &time_formatter).to_string() ))); } if let Ok(time) = metadata.modified() { details = details.push(widget::text::body(fl!( "item-modified", modified = format_time(time, &date_time_formatter, &time_formatter).to_string() ))); } if let Ok(time) = metadata.accessed() { details = details.push(widget::text::body(fl!( "item-accessed", accessed = format_time(time, &date_time_formatter, &time_formatter).to_string() ))); } #[cfg(unix)] if let Some(path) = self.path_opt() { use std::os::unix::fs::MetadataExt; let mode = metadata.mode(); let user_name = uzers::get_user_by_uid(metadata.uid()) .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(); let user_path = path.clone(); settings.push( widget::settings::item::builder(user_name) .description(fl!("owner")) .control(widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), Some(get_mode_part(mode, MODE_SHIFT_USER).try_into().unwrap()), move |selected| { Message::SetPermissions( user_path.clone(), set_mode_part( mode, MODE_SHIFT_USER, selected.try_into().unwrap(), ), ) }, )), ); let group_name = uzers::get_group_by_gid(metadata.gid()) .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(); let group_path = path.clone(); settings.push( widget::settings::item::builder(group_name) .description(fl!("group")) .control(widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), Some(get_mode_part(mode, MODE_SHIFT_GROUP).try_into().unwrap()), move |selected| { Message::SetPermissions( group_path.clone(), set_mode_part( mode, MODE_SHIFT_GROUP, selected.try_into().unwrap(), ), ) }, )), ); let other_path = path.clone(); settings.push(widget::settings::item::builder(fl!("other")).control( widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), Some(get_mode_part(mode, MODE_SHIFT_OTHER).try_into().unwrap()), move |selected| { Message::SetPermissions( other_path.clone(), set_mode_part(mode, MODE_SHIFT_OTHER, selected.try_into().unwrap()), ) }, ), )); } } if let Some(path) = self.path_opt() && let Ok(img) = image::image_dimensions(path) { let (width, height) = img; details = details.push(widget::text::body(format!("{width}x{height}"))); } column = column.push(details); if let Some(path) = self.path_opt() && self.selected { column = column.push( widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))), ); } if !settings.is_empty() { let mut section = widget::settings::section(); section = section.extend(settings); column = column.push(section); } column.into() } pub fn replace_view(&self, heading: String, military_time: bool) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing(); let mut row = widget::row::with_capacity(2).spacing(space_xxxs); row = row.push(self.preview()); let mut column = widget::column::with_capacity(3).spacing(space_xxxs); column = column.push(widget::text::heading(heading)); //TODO: translate! //TODO: correct display of folder size? if let ItemMetadata::Path { metadata, children_opt, } = &self.metadata { if metadata.is_dir() { if let Some(children) = children_opt { column = column.push(widget::text::body(format!("Items: {children}"))); } } else { column = column.push(widget::text::body(format!( "Size: {}", format_size(metadata.len()) ))); } if let Ok(time) = metadata.modified() { let date_time_formatter = date_time_formatter(military_time); let time_formatter = time_formatter(military_time); column = column.push(widget::text::body(format!( "Last modified: {}", format_time(time, &date_time_formatter, &time_formatter) ))); } } else { //TODO: other metadata } row = row.push(column); row.into() } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] pub enum View { Grid, List, } #[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)] pub enum HeadingOptions { Name = 0, Modified, Size, TrashedOn, } impl fmt::Display for HeadingOptions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Name => write!(f, "{}", fl!("name")), Self::Modified => write!(f, "{}", fl!("modified")), Self::Size => write!(f, "{}", fl!("size")), Self::TrashedOn => write!(f, "{}", fl!("trashed-on")), } } } impl HeadingOptions { pub fn names() -> Vec { vec![ Self::Name.to_string(), Self::Modified.to_string(), Self::Size.to_string(), Self::TrashedOn.to_string(), ] } } #[derive(Clone, Debug)] pub enum Mode { App, Desktop, Dialog(DialogKind), } impl Mode { /// Whether multiple files can be selected in this mode pub fn multiple(&self) -> bool { match self { Self::App | Self::Desktop => true, Self::Dialog(dialog) => dialog.multiple(), } } } struct SearchContext { results_rx: mpsc::Receiver, ready: Arc, last_modified_opt: Arc>>, } pub struct SearchContextWrapper(Option); impl Clone for SearchContextWrapper { fn clone(&self) -> Self { Self(None) } } impl fmt::Debug for SearchContextWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SearchContextWrapper").finish() } } // TODO when creating items, pass > to each item // as a drag data, so that when dnd is initiated, they are all included pub struct Tab { //TODO: make more items private pub location: Location, pub location_ancestors: Vec<(Location, String)>, pub location_title: String, pub location_context_menu_point: Option, pub location_context_menu_index: Option, pub context_menu: Option, pub mode: Mode, pub scroll_opt: Option, pub size_opt: Cell>, pub content_height_opt: Cell>, pub viewport_opt: Option, pub item_view_size_opt: Cell>, pub edit_location: Option, pub edit_location_id: widget::Id, pub history_i: usize, pub history: Vec, pub config: TabConfig, pub thumb_config: ThumbCfg, pub sort_name: HeadingOptions, pub sort_direction: bool, pub gallery: bool, pub(crate) parent_item_opt: Option>, pub(crate) items_opt: Option>, pub dnd_hovered: Option<(Location, Instant)>, pub(crate) scrollable_id: widget::Id, select_focus: Option, select_range: Option<(usize, usize)>, clicked: Option, selected_clicked: bool, last_right_click: Option, search_context: Option, date_time_formatter: DateTimeFormatter, time_formatter: DateTimeFormatter, watch_drag: bool, window_id: Option, large_image_manager: LargeImageManager, } async fn calculate_dir_size(path: &Path, controller: Controller) -> Result { let mut total = 0; for entry_res in WalkDir::new(path) { controller .check() .await .map_err(|s| OperationError::from_state(s, &controller))?; //TODO: report more errors? if let Ok(entry) = entry_res && let Ok(metadata) = entry.metadata() && metadata.is_file() { total += metadata.len(); } // Yield in case this process takes a while. tokio::task::yield_now().await; } Ok(total) } fn folder_name>(path: P) -> (String, bool) { let path = path.as_ref(); let mut found_home = false; let name = match path.file_name() { Some(name) => { if path == crate::home_dir() { found_home = true; fl!("home") } else { match (get_filename_from_path(path), fs::metadata(path)) { (Ok(name), Ok(metadata)) => { let is_gvfs = fs_kind(&metadata) == FsKind::Gvfs; display_name_for_file(path, &name, is_gvfs, false) } _ => name.to_string_lossy().into_owned(), } } } None => { fl!("filesystem") } }; (name, found_home) } // parse .hidden file and return files path pub fn parse_hidden_file(path: &PathBuf) -> Box<[String]> { let Ok(file) = File::open(path) else { return Default::default(); }; BufReader::new(file) .lines() .map_while(Result::ok) .filter_map(|line| { let line = line.trim(); (!line.is_empty()).then_some(line.to_owned()) }) .collect() } impl Tab { pub fn new( location: Location, config: TabConfig, thumb_config: ThumbCfg, sorting_options: Option<&FxOrderMap>, scrollable_id: widget::Id, window_id: Option, ) -> Self { let location_str = location.to_string(); let (sort_name, sort_direction) = sorting_options .and_then(|opts| opts.get(&location_str)) .or_else(|| SORT_OPTION_FALLBACK.get(&location_str)) .copied() .unwrap_or((HeadingOptions::Name, true)); let location = location.normalize(); let location_ancestors = location.ancestors(); let location_title = location.title(); let history = vec![location.clone()]; Self { location, location_ancestors, location_title, context_menu: None, location_context_menu_point: None, location_context_menu_index: None, mode: Mode::App, scroll_opt: None, size_opt: Cell::new(None), content_height_opt: Cell::new(None), viewport_opt: None, item_view_size_opt: Cell::new(None), edit_location: None, edit_location_id: widget::Id::unique(), history_i: 0, history, config, thumb_config, sort_name, sort_direction, gallery: false, parent_item_opt: None, items_opt: None, scrollable_id, select_focus: None, select_range: None, clicked: None, dnd_hovered: None, selected_clicked: false, last_right_click: None, search_context: None, date_time_formatter: date_time_formatter(config.military_time), time_formatter: time_formatter(config.military_time), watch_drag: true, window_id, large_image_manager: LargeImageManager::new(), } } pub fn title(&self) -> String { //TODO: is it possible to return a &str? self.location_title.clone() } pub const fn items_opt(&self) -> Option<&Vec> { self.items_opt.as_ref() } pub const fn items_opt_mut(&mut self) -> Option<&mut Vec> { self.items_opt.as_mut() } pub fn set_items(&mut self, mut items: Vec) { let highlighted = self .items_opt .as_ref() .and_then(|items| items.iter().enumerate().find(|i| i.1.highlighted)) .map(|(i, _)| i); let selected = self.selected_locations(); for item in &mut items { item.selected = false; if let Some(location) = &item.location_opt && selected.contains(location) { item.selected = true; } } self.items_opt = Some(items); if let Some(i) = highlighted .zip(self.items_opt.as_mut()) .and_then(|(h, items)| items.get_mut(h)) { i.highlighted = true; } } pub fn cut_selected(&mut self) { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.cut = item.selected; } } } pub fn refresh_cut(&mut self, locations: &[PathBuf]) { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.cut = false; if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt) && locations.contains(location_path) { item.cut = true; } } } } pub fn selected_locations(&self) -> Vec { if let Some(ref items) = self.items_opt { items .iter() .filter_map(|item| { if item.selected { item.location_opt.clone() } else { None } }) .collect() } else { Vec::new() } } pub fn select_all(&mut self) { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { if !self.config.show_hidden && item.hidden { item.selected = false; continue; } item.selected = true; } } } pub fn select_none(&mut self) -> bool { self.select_focus = None; let mut had_selection = false; if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { if item.selected { item.selected = false; had_selection = true; } } } had_selection } pub fn select_name(&mut self, name: &str) { self.select_focus = None; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { item.selected = item.name == name; if item.selected { self.select_focus = Some(i); } } } } /// Selects the first item whose name starts with the given prefix (case-insensitive). /// Returns true if an item was selected. pub fn select_by_prefix(&mut self, prefix: &str) -> bool { let prefix_lower = prefix.to_lowercase(); let focus = self.select_focus.take(); if let Some(ref mut items) = self.items_opt { // First, deselect all items for item in items.iter_mut() { item.selected = false; } // Determine the start index of the search. When the index is before the currently focused item, it will be // considered first, otherwise last. Consider the focused item last when only a single character has been // typed, so we eagerly switch focus on the first character and stay on the same item as long as the prefix // matches. let single_char = prefix_lower.chars().count() == 1; let start = if single_char { Self::index_after_focus(focus, self.sort_direction) } else { Self::index_before_focus(focus, self.sort_direction) }; self.select_focus = Self::select_first_prefix_from_index( &prefix_lower, items, start, self.sort_direction, ); if self.select_focus.is_some() || single_char { return self.select_focus.is_some(); } let mut chars = prefix_lower.chars(); let Some(first) = chars.next() else { log::error!("search term is empty"); return self.select_focus.is_some(); }; // Check if all entered characters are the same if !chars.all(|c| c == first) { return self.select_focus.is_some(); } // Search for a single character when all entered characters are the same. // This allows cycling through items starting with the same character by repeatedly pressing a key. let start = Self::index_after_focus(focus, self.sort_direction); self.select_focus = Self::select_first_prefix_from_index( &first.to_string(), items, start, self.sort_direction, ); return self.select_focus.is_some(); } false } fn index_before_focus(current_focus: Option, forward: bool) -> usize { current_focus.map_or(0, |i| if forward { i } else { i + 1 }) } fn index_after_focus(current_focus: Option, forward: bool) -> usize { current_focus.map_or(0, |i| if forward { i + 1 } else { i }) } fn select_first_prefix_from_index( prefix_lower: &str, items: &mut [Item], start: usize, forward: bool, ) -> Option { // Order the search item so they begin at `start`. let Some((until, after)) = items.split_at_mut_checked(start) else { log::error!( "invalid start index {start} for items of length {}", items.len() ); return None; }; let search_items = after .iter_mut() .enumerate() .map(|(i, item)| (i + start, item)) .chain(until.iter_mut().enumerate()); if forward { Self::select_first_prefix_match(prefix_lower, search_items) } else { Self::select_first_prefix_match(prefix_lower, search_items.rev()) } } /// Selects the first item in the given iterator whose name starts with the given prefix. /// /// The `prefix` must be lowercase. fn select_first_prefix_match<'a>( prefix: &str, items: impl Iterator, ) -> Option { for (i, item) in items { if item.name.to_lowercase().starts_with(prefix) { item.selected = true; return Some(i); } } None } pub fn select_paths(&mut self, paths: Vec) { self.select_focus = None; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { item.selected = false; if let Some(path) = item.path_opt() && paths.contains(path) { item.selected = true; self.select_focus = Some(i); } } } } fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool { let mut start = (row, col); let mut end = (row, col); if mod_shift { if self.select_focus.is_none() || self.select_range.is_none() { // Set select range to initial state if necessary self.select_range = self.select_focus.map(|i| (i, i)); } if let Some(pos) = self.select_range_start_pos_opt() { if pos.0 < row || (pos.0 == row && pos.1 < col) { start = pos; } else { end = pos; } } } let mut found = false; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { item.selected = false; let pos = match item.pos_opt.get() { Some(some) => some, None => continue, }; if pos.0 < start.0 || (pos.0 == start.0 && pos.1 < start.1) { // Before start continue; } if pos.0 > end.0 || (pos.0 == end.0 && pos.1 > end.1) { // After end continue; } if pos == (row, col) { // Update focus if this is what we wanted to select self.select_focus = Some(i); self.select_range = if mod_shift { self.select_range.map(|r| (r.0, i)) } else { Some((i, i)) }; found = true; } item.selected = true; } } found } pub fn select_rect(&mut self, rect: Rectangle, mod_ctrl: bool, mod_shift: bool) { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { let was_overlapped = item.overlaps_drag_rect; item.overlaps_drag_rect = item.rect_opt.get().is_some_and(|r| r.intersects(&rect)); item.selected = if mod_ctrl || mod_shift { if was_overlapped == item.overlaps_drag_rect { item.selected } else { !item.selected } } else { item.overlaps_drag_rect }; } } } pub fn select_focus_id(&self) -> Option { let items = self.items_opt.as_ref()?; let item = items.get(self.select_focus?)?; Some(item.button_id.clone()) } fn select_focus_pos_opt(&self) -> Option<(usize, usize)> { let items = self.items_opt.as_ref()?; let item = items.get(self.select_focus?)?; item.pos_opt.get() } pub(crate) fn select_focus_scroll(&mut self) -> Option { let items = self.items_opt.as_ref()?; let item = items.get(self.select_focus?)?; let rect = item.rect_opt.get()?; //TODO: move to function let visible_rect = { let point = match self.scroll_opt { Some(offset) => Point::new(0.0, offset.y), None => Point::new(0.0, 0.0), }; let size = self .item_view_size_opt .get() .unwrap_or_else(|| Size::new(0.0, 0.0)); Rectangle::new(point, size) }; if rect.y < visible_rect.y { // Scroll up to rect self.scroll_opt = Some(AbsoluteOffset { x: 0.0, y: rect.y }); self.scroll_opt } else if (rect.y + rect.height) > (visible_rect.y + visible_rect.height) { // Scroll down to rect self.scroll_opt = Some(AbsoluteOffset { x: 0.0, y: rect.y + rect.height - visible_rect.height, }); self.scroll_opt } else { // Do not scroll None } } fn select_range_start_pos_opt(&self) -> Option<(usize, usize)> { let items = self.items_opt.as_ref()?; let item = items.get(self.select_range.map(|r| r.0)?)?; item.pos_opt.get() } fn select_first_pos_opt(&self) -> Option<(usize, usize)> { let items = self.items_opt.as_ref()?; let mut first = None; for item in items { if !item.selected { continue; } let (row, col) = match item.pos_opt.get() { Some(some) => some, None => continue, }; first = Some(match first { Some((first_row, first_col)) => match row.cmp(&first_row) { Ordering::Less => (row, col), Ordering::Equal => (row, col.min(first_row)), Ordering::Greater => (first_row, first_col), }, None => (row, col), }); } first } fn select_last_pos_opt(&self) -> Option<(usize, usize)> { let items = self.items_opt.as_ref()?; let mut last = None; for item in items { if !item.selected { continue; } let (row, col) = match item.pos_opt.get() { Some(some) => some, None => continue, }; last = Some(match last { Some((last_row, last_col)) => match row.cmp(&last_row) { Ordering::Greater => (row, col), Ordering::Equal => (row, col.max(last_row)), Ordering::Less => (last_row, last_col), }, None => (row, col), }); } last } fn trigger_async_decode(&mut self) -> Vec { // Only trigger decode in gallery mode for the currently selected image if !self.gallery { return Vec::new(); } let Some(index) = self.select_focus else { return Vec::new(); }; let Some(items) = &self.items_opt else { return Vec::new(); }; let Some(item) = items.get(index) else { return Vec::new(); }; let Some(ItemThumbnail::Image(_, original_dims)) = &item.thumbnail_opt else { return Vec::new(); }; if let Some((w, h)) = original_dims && !should_use_tiling(*w, *h) { return Vec::new(); } let Some(path) = item.path_opt() else { return Vec::new(); }; // Clone path to avoid borrow checker issues let path = path.to_path_buf(); // Get display size for adaptive resolution let display_dimensions = self .size_opt .get() .map(|size| (size.width as u32, size.height as u32)); // Try to decode the image using LargeImageManager with adaptive resolution let (should_decode, target_dimensions, generation) = self .large_image_manager .try_decode(&path, display_dimensions); if should_decode { vec![Command::Iced( cosmic::iced::Task::perform( decode_large_image(path, target_dimensions), move |result| { result .map(|(path, width, height, pixels)| { Message::ImageDecoded( path, width, height, pixels, display_dimensions, generation, ) }) .unwrap_or_else(|| Message::AutoScroll(None)) }, ) .into(), )] } else { Vec::new() } } pub fn change_location(&mut self, location: &Location, history_i_opt: Option) { self.location = location.normalize(); self.location_ancestors = self.location.ancestors(); self.location_title = self.location.title(); self.context_menu = None; self.edit_location = None; self.items_opt = None; //TODO: remember scroll by location? self.scroll_opt = None; self.select_focus = None; self.search_context = None; if let Some(history_i) = history_i_opt { // Navigating in history self.history_i = history_i; } else { // Truncate history to remove next entries self.history.truncate(self.history_i + 1); // Compact consecutive matching paths { let mut remove = false; if let Some(last_location) = self.history.last() { if let Location::Network(last_uri, ..) = last_location && let Location::Network(uri, ..) = location { remove = last_uri == uri; } else if let Some(last_path) = last_location.path_opt() && let Some(path) = location.path_opt() { remove = last_path == path; } } if remove { self.history.pop(); } } // Push to the front of history self.history_i = self.history.len(); self.history.push(location.clone()); } } pub fn update(&mut self, message: Message, modifiers: Modifiers) -> Vec { let mut commands = Vec::new(); let mut cd = None; let mut history_i_opt = None; let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); let last_context_menu = self.context_menu; match message { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); } Message::AutoScroll(auto_scroll) => { commands.push(Command::AutoScroll(auto_scroll)); } Message::ClickRelease(click_i_opt) => { // Single click to open. if !mod_ctrl && self.config.single_click { let mut paths_to_open = Vec::new(); if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { if Some(i) == click_i_opt { if let Some(location) = &item.location_opt { if item.metadata.is_dir() { cd = Some(location.clone()); } else if let Some(path) = location.path_opt() { paths_to_open.push(path.clone()); } else { log::warn!("no path for item {item:?}"); } } else { log::warn!("no location for item {item:?}"); } } } } if !paths_to_open.is_empty() { commands.push(Command::OpenFile(paths_to_open)); } } if click_i_opt != self.clicked.take() { self.context_menu = None; self.location_context_menu_index = None; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { if mod_ctrl { if Some(i) == click_i_opt && item.selected { item.selected = false; self.select_range = None; } } else if Some(i) != click_i_opt { item.selected = false; } } } } } Message::DragEnd => { self.clicked = None; self.watch_drag = true; } Message::DoubleClick(click_i_opt) => { if let Some(clicked_item) = self .items_opt .as_ref() .and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i))) { if let Some(location) = &clicked_item.location_opt { if clicked_item.metadata.is_dir() { cd = Some(location.clone()); } else if let Some(path) = location.path_opt() { commands.push(Command::OpenFile(vec![path.clone()])); } else { log::warn!("no path for item {clicked_item:?}"); } } else { log::warn!("no location for item {clicked_item:?}"); } } else { log::warn!("no item for click index {click_i_opt:?}"); } } Message::Click(click_i_opt) => { self.selected_clicked = false; self.context_menu = None; self.edit_location = None; self.location_context_menu_index = None; if click_i_opt.is_none() { self.clicked = click_i_opt; } if mod_shift { if let Some(click_i) = click_i_opt { self.select_range = self .select_range .map_or(Some((click_i, click_i)), |r| Some((r.0, click_i))); if let Some(range) = self.select_range { let range_min = range.0.min(range.1); let range_max = range.0.max(range.1); // A sorted tab's items can't be linearly selected // Let's say we have: // index | file // 0 | file0 // 1 | file1 // 2 | file2 // This is both the default sort and internal ordering // When sorted it may be displayed as: // 1 | file1 // 0 | file0 // 2 | file2 // However, the internal ordering is still the same thus // linearly selecting items doesn't work. Shift selecting // file0 and file2 would select indices 0 to 2 when it should // select indices 0 AND 2 from items_opt let indices: Vec<_> = self .column_sort() .map(|sorted| sorted.into_iter().map(|(i, _)| i).collect()) .unwrap_or_else(|| { let len = self .items_opt .as_deref() .map(<[Item]>::len) .unwrap_or_default(); (0..len).collect() }); // Find the true indices for the min and max element w.r.t. // a sorted tab. let min = indices .iter() .copied() .position(|offset| offset == range_min) .unwrap_or_default(); // We can't skip `min_real` elements here because the index of // `max` may actually be before `min` in a sorted tab let max = indices .iter() .copied() .position(|offset| offset == range_max) .unwrap_or(indices.len()); let min_real = min.min(max); let max_real = max.max(min); if let Some(ref mut items) = self.items_opt { for index in indices .into_iter() .skip(min_real) .take(max_real - min_real + 1) { if let Some(item) = items.get_mut(index) { if item.hidden { if self.config.show_hidden { item.selected = true; } } else { item.selected = true; } } } } } self.clicked = click_i_opt; self.select_focus = click_i_opt; self.selected_clicked = true; } } else { let dont_unset = mod_ctrl || self.column_sort().is_some_and(|l| { l.iter() .any(|&(e_i, e)| Some(e_i) == click_i_opt && e.selected) }); if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { if Some(i) == click_i_opt { // Filter out selection if it does not match dialog kind if let Mode::Dialog(dialog) = &self.mode { let item_is_dir = item.metadata.is_dir(); if item_is_dir != dialog.is_dir() { // Allow selecting folder if dialog is for files to make it // possible to double click //TODO: clear any other selection when selecting a folder if !item_is_dir { continue; } } } if !item.selected { self.clicked = click_i_opt; item.selected = true; } self.select_range = Some((i, i)); self.select_focus = click_i_opt; self.selected_clicked = true; } else if !dont_unset && item.selected { self.clicked = click_i_opt; item.selected = false; } } } } } Message::Config(config) => { // View is preserved for existing tabs let view = self.config.view; let military_time_changed = self.config.military_time != config.military_time; let show_hidden_changed = self.config.show_hidden != config.show_hidden; self.config = config; self.config.view = view; if military_time_changed { self.date_time_formatter = date_time_formatter(self.config.military_time); self.time_formatter = time_formatter(self.config.military_time); } if show_hidden_changed && let Location::Search(path, term, ..) = &self.location { cd = Some(Location::Search( path.clone(), term.clone(), self.config.show_hidden, Instant::now(), )); } // Unhighlight all items when config changes if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.highlighted = false; } } } Message::ContextAction(action) => { // Close context menu self.context_menu = None; commands.push(Command::Action(action)); } Message::RunContextAction(action) => { self.context_menu = None; commands.push(Command::RunContextAction(action)); } Message::ContextMenu(point_opt, _) => { self.edit_location = None; self.context_menu = point_opt; self.location_context_menu_index = None; //TODO: hack for clearing selecting when right clicking empty space if self.context_menu.is_some() && self.last_right_click.take().is_none() && let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.selected = false; } } } Message::LocationContextMenuPoint(point_opt) => { self.context_menu = None; self.location_context_menu_point = point_opt; } Message::LocationContextMenuIndex(p, index_opt) => { self.context_menu = None; self.location_context_menu_point = p; self.location_context_menu_index = index_opt; } Message::LocationMenuAction(action) => { self.location_context_menu_index = None; let path_for_index = |ancestor_index| { self.location .path_opt() .and_then(|path| path.ancestors().nth(ancestor_index)) .map(Path::to_path_buf) }; match action { LocationMenuAction::OpenInNewTab(ancestor_index) => { if let Some(path) = path_for_index(ancestor_index) { commands.push(Command::OpenInNewTab(path)); } } LocationMenuAction::OpenInNewWindow(ancestor_index) => { if let Some(path) = path_for_index(ancestor_index) { commands.push(Command::OpenInNewWindow(path)); } } LocationMenuAction::Preview(ancestor_index) => { if let Some(path) = path_for_index(ancestor_index) { //TODO: blocking code, run in command match item_from_path(&path, IconSizes::default()) { Ok(item) => { commands.push(Command::Preview(PreviewKind::Custom( PreviewItem(Box::new(item)), ))); } Err(err) => { log::warn!( "failed to get item from path {}: {}", path.display(), err ); } } } } LocationMenuAction::AddToSidebar(ancestor_index) => { if let Some(path) = path_for_index(ancestor_index) { commands.push(Command::AddToSidebar(path)); } else { log::warn!( "no ancestor {ancestor_index} for location {:?}", self.location ); } } } } Message::Drag(rect_opt) => { self.watch_drag = false; if let Some(rect) = rect_opt { self.context_menu = None; self.location_context_menu_index = None; if self.mode.multiple() { self.select_rect(rect, mod_ctrl, mod_shift); } if self.select_focus.take().is_some() { // Unfocus currently focused button commands.push(Command::Iced( widget::button::focus(widget::Id::unique()).into(), )); } } } Message::EditLocation(edit_location) => { self.edit_location = edit_location; if self.edit_location.is_some() { commands.push(Command::Iced( widget::text_input::focus(self.edit_location_id.clone()).into(), )); } } Message::EditLocationComplete(selected) => { if let Some(mut edit_location) = self.edit_location.take() && !matches!(edit_location.location, Location::Network(..)) { edit_location.selected = Some(selected); cd = edit_location.resolve(); } } Message::EditLocationEnable => { commands.push(Command::Iced( widget::text_input::focus(self.edit_location_id.clone()).into(), )); self.edit_location = Some(self.location.clone().into()); } Message::EditLocationSubmit => { if let Some(mut edit_location) = self.edit_location.take() { // Select first completion if current location does not exist if edit_location.selected.is_none() && edit_location .completions .as_ref() .is_some_and(|completions| !completions.is_empty()) && edit_location .location .path_opt() .is_some_and(|path| !path.exists()) { edit_location.selected = Some(0); } cd = edit_location.resolve(); } } Message::EditLocationTab => { if let Some(edit_location) = &mut self.edit_location { edit_location.select(!modifiers.contains(Modifiers::SHIFT)); } } Message::OpenInNewTab(path) => { commands.push(Command::OpenInNewTab(path)); } Message::EmptyTrash => { commands.push(Command::EmptyTrash); } #[cfg(feature = "desktop")] Message::ExecEntryAction(path, action) => { let lang_id = crate::localize::LANGUAGE_LOADER.current_language(); let language = lang_id.language.as_str(); match path.map_or_else( || { let items = self.items_opt.as_deref()?; items.iter().find(|&item| item.selected).and_then(|item| { let location = item.location_opt.as_ref()?; let path = location.path_opt()?; cosmic::desktop::load_desktop_file(&[language.into()], path.into()) }) }, |path| cosmic::desktop::load_desktop_file(&[language.into()], path), ) { Some(entry) => commands.push(Command::ExecEntryAction(entry, action)), None => log::warn!("Invalid desktop entry path passed to ExecEntryAction"), } } Message::Gallery(gallery) => { self.gallery = gallery; if gallery { commands.extend(self.trigger_async_decode()); } } Message::GalleryPrevious | Message::GalleryNext => { let mut pos_opt = None; if let Some(mut indices) = self.column_sort() { if matches!(message, Message::GalleryPrevious) { indices.reverse(); } let mut found = false; for (index, item) in indices { if self.select_focus.is_none() { found = true; } if self.select_focus == Some(index) { found = true; continue; } if found && item.can_gallery() { pos_opt = item.pos_opt.get(); if pos_opt.is_some() { break; } } } } if let Some((row, col)) = pos_opt { // Should mod_shift be available? self.select_position(row, col, mod_shift); commands.extend(self.trigger_async_decode()); } if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } Message::GalleryToggle => { if let Some(indices) = self.column_sort() { for (_, item) in &indices { if item.selected && item.can_gallery() { self.gallery = !self.gallery; if self.gallery { commands.extend(self.trigger_async_decode()); } break; } } } } Message::GoNext => { if let Some(history_i) = self.history_i.checked_add(1) && let Some(location) = self.history.get(history_i) { cd = Some(location.clone()); history_i_opt = Some(history_i); } } Message::GoPrevious => { if let Some(history_i) = self.history_i.checked_sub(1) && let Some(location) = self.history.get(history_i) { cd = Some(location.clone()); history_i_opt = Some(history_i); } } Message::ItemDown => { if let Some(edit_location) = &mut self.edit_location { edit_location.select(true); } else if self.gallery { commands.append(&mut self.update(Message::GalleryNext, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt()) { if self.select_focus.is_none() { // Select last item in current selection to focus it. self.select_position(row, col, mod_shift); } //TODO: Shift modifier should select items in between // Try to select item in next row if !self.select_position(row + 1, col, mod_shift) { // Ensure current item is still selected if there are no other items self.select_position(row, col, mod_shift); } } else { // Select first item //TODO: select first in scroll self.select_position(0, 0, mod_shift); } if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::ItemLeft => { if self.gallery { commands.append(&mut self.update(Message::GalleryPrevious, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_first_pos_opt()) { if self.select_focus.is_none() { // Select first item in current selection to focus it. self.select_position(row, col, mod_shift); } // Try to select previous item in current row if !col .checked_sub(1) .is_some_and(|col| self.select_position(row, col, mod_shift)) { // Try to select last item in previous row if !row.checked_sub(1).is_some_and(|row| { let mut col = 0; if let Some(ref items) = self.items_opt { for item in items { match item.pos_opt.get() { Some((item_row, item_col)) if item_row == row => { col = col.max(item_col); } _ => continue, } } } self.select_position(row, col, mod_shift) }) { // Ensure current item is still selected if there are no other items self.select_position(row, col, mod_shift); } } } else { // Select first item //TODO: select first in scroll self.select_position(0, 0, mod_shift); } if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::ItemRight => { if self.gallery { commands.append(&mut self.update(Message::GalleryNext, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt()) { if self.select_focus.is_none() { // Select last item in current selection to focus it. self.select_position(row, col, mod_shift); } // Try to select next item in current row if !self.select_position(row, col + 1, mod_shift) { // Try to select first item in next row if !self.select_position(row + 1, 0, mod_shift) { // Ensure current item is still selected if there are no other items self.select_position(row, col, mod_shift); } } } else { // Select first item //TODO: select first in scroll self.select_position(0, 0, mod_shift); } if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::ItemUp => { if let Some(edit_location) = &mut self.edit_location { edit_location.select(false); } else if self.gallery { commands.append(&mut self.update(Message::GalleryPrevious, modifiers)); } else { if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_first_pos_opt()) { if self.select_focus.is_none() { // Select first item in current selection to focus it. self.select_position(row, col, mod_shift); } //TODO: Shift modifier should select items in between // Try to select item in last row if !row .checked_sub(1) .is_some_and(|row| self.select_position(row, col, mod_shift)) { // Ensure current item is still selected if there are no other items self.select_position(row, col, mod_shift); } } else { // Select first item //TODO: select first in scroll self.select_position(0, 0, mod_shift); } if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::Location(location) => { // Workaround to support favorited files match &location { Location::Path(path) => { if path.is_dir() { cd = Some(location); } else { commands.push(Command::OpenFile(vec![path.clone()])); } } _ => { cd = Some(location); } } } Message::LocationUp => { // Sets location to the path's parent // Does nothing if path is root or location is Trash if let Location::Path(ref path) = self.location && let Some(parent) = path.parent() { cd = Some(Location::Path(parent.to_owned())); } } Message::Open(path_opt) => { match path_opt { Some(path) => { if path.is_dir() { cd = Some(Location::Path(path)); } else { commands.push(Command::OpenFile(vec![path])); } } // Open selected items None => { enum ResolveResult { Open(Option), OpenInTab(Option), OpenTrash, OpenProperties, Cd(Location), Skip, } fn resolve_item( item: &Item, mode: &Mode, is_only_one_selected: bool, ) -> ResolveResult { if !item.selected { return ResolveResult::Skip; } let location = match &item.location_opt { Some(l) => l, None => return ResolveResult::OpenProperties, }; let path_opt = location.path_opt(); if item.metadata.is_dir() { match mode { Mode::App => { if is_only_one_selected { ResolveResult::Cd(location.clone()) } else { ResolveResult::OpenInTab(path_opt.cloned()) } } Mode::Desktop => match location { Location::Trash => ResolveResult::OpenTrash, _ => ResolveResult::Open(path_opt.cloned()), }, Mode::Dialog(_) => { if is_only_one_selected { ResolveResult::Cd(location.clone()) } else { ResolveResult::Skip } } } } else { ResolveResult::Open(path_opt.cloned()) } } let mut open_files = Vec::new(); if let Some(items) = self.items_opt.as_ref() { let selected_count = items.iter().filter(|i| i.selected).count(); for item in items.iter() { match resolve_item(item, &self.mode, selected_count == 1) { ResolveResult::Open(Some(p)) => open_files.push(p), ResolveResult::OpenInTab(Some(p)) => { commands.push(Command::OpenInNewTab(p)) } ResolveResult::Cd(loc) => cd = Some(loc), ResolveResult::OpenTrash => commands.push(Command::OpenTrash), ResolveResult::OpenProperties => {} //TODO: open properties? _ => {} } } } if !open_files.is_empty() { commands.push(Command::OpenFile(open_files)); } } } } Message::Reload => { //TODO: support keeping selected locations without paths let selected_paths = self .selected_locations() .into_iter() .filter_map(Location::into_path_opt) .collect(); let location = self.location.clone(); self.change_location(&location, None); commands.push(Command::ChangeLocation( self.title(), location, Some(selected_paths), )); } Message::RightClick(_point_opt, click_i_opt) => { if mod_ctrl || mod_shift { self.update(Message::Click(click_i_opt), modifiers); } if let Some(ref mut items) = self.items_opt && !click_i_opt .is_some_and(|click_i| items.get(click_i).is_some_and(|x| x.selected)) { // If item not selected, clear selection on other items for (i, item) in items.iter_mut().enumerate() { item.selected = Some(i) == click_i_opt; } } //TODO: hack for clearing selecting when right clicking empty space self.last_right_click = click_i_opt; } Message::MiddleClick(click_i) => { if mod_ctrl || mod_shift { self.update(Message::Click(Some(click_i)), modifiers); } else { if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { item.selected = i == click_i; } self.select_range = Some((click_i, click_i)); } if let Some(clicked_item) = self.items_opt.as_ref().and_then(|items| items.get(click_i)) { if let Some(path) = clicked_item.path_opt() { if clicked_item.metadata.is_dir() { //cd = Some(Location::Path(path.clone())); commands.push(Command::OpenInNewTab(path.clone())); } else { commands.push(Command::OpenFile(vec![path.clone()])); } } else { log::warn!("no path for item {clicked_item:?}"); } } else { log::warn!("no item for click index {click_i:?}"); } } } Message::HighlightDeactivate(i) => { self.watch_drag = true; if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) { item.highlighted = false; } } Message::HighlightActivate(i) => { self.watch_drag = true; if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) { item.highlighted = true; } } Message::Resize(viewport) => { // Scroll to ensure focused item still in view if self.viewport_opt.map(|v| v.size()) != Some(viewport.size()) && let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } self.viewport_opt = Some(viewport); } Message::Scroll(viewport) => { self.scroll_opt = Some(viewport.absolute_offset()); self.watch_drag = true; } Message::ScrollTab(scroll_speed) => { commands.push(Command::Iced( scrollable::scroll_by( self.scrollable_id.clone(), AbsoluteOffset { x: 0.0, y: scroll_speed, }, ) .into(), )); } Message::ScrollToFocused => { if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } } Message::SearchContext(location, context) => { if location == self.location { self.search_context = context.0; } else { log::warn!( "search context provided for {:?} instead of {:?}", location, self.location ); } } Message::SearchReady(finished) => { if let Some(context) = &mut self.search_context { if let Some(items) = &mut self.items_opt { if finished || context.ready.swap(false, atomic::Ordering::SeqCst) { let duration = Instant::now(); while let Ok(search_item) = context.results_rx.try_recv() { //TODO: combine this with column_sort logic, they must match! let index = if let SearchItem::Path(_, _, ref metadata) = search_item { let item_modified = metadata.modified().ok(); match items.binary_search_by(|other| { item_modified.cmp(&other.metadata.modified()) }) { Ok(index) => index, Err(index) => index, } } else { items.len() }; if index < MAX_SEARCH_RESULTS { //TODO: use correct IconSizes let item = item_from_search_item(search_item, IconSizes::default()); items.insert(index, item); } // Ensure that updates make it to the GUI in a timely manner if !finished && duration.elapsed() >= MAX_SEARCH_LATENCY { break; } } } if items.len() >= MAX_SEARCH_RESULTS { items.truncate(MAX_SEARCH_RESULTS); if let Some(last_modified) = items.last().and_then(|item| item.metadata.modified()) { *context.last_modified_opt.write().unwrap() = Some(last_modified); } } } else { log::warn!("search ready but items array is empty"); } } if finished { self.search_context = None; } } Message::SelectAll => { self.select_all(); if self.select_focus.take().is_some() { // Unfocus currently focused button commands.push(Command::Iced( widget::button::focus(widget::Id::unique()).into(), )); } } Message::SelectFirst => { if self.select_position(0, 0, mod_shift) { if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::SelectLast => { if let Some(ref items) = self.items_opt && let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max() && self.select_position(last_pos.0, last_pos.1, mod_shift) { if let Some(offset) = self.select_focus_scroll() { commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(offset.x), y: Some(offset.y), }, ) .into(), )); } if let Some(id) = self.select_focus_id() { commands.push(Command::Iced(widget::button::focus(id).into())); } } } Message::SetOpenWith(mime, id) => { commands.push(Command::SetOpenWith(mime, id)); } Message::SetPermissions(path, mode) => { commands.push(Command::SetPermissions(path, mode)); } Message::ShiftPermissions(path_mode_opt, shift, bits) => match path_mode_opt { Some((path, mode)) => commands.push(Command::SetPermissions( path, set_mode_part(mode, shift, bits), )), // Shift permissions on all selected items None => { let mut permissions = Vec::new(); for item in self.items_opt().map_or(Vec::new(), |items| { items.iter().filter(|item| item.selected).collect() }) { #[cfg(unix)] if let (Some(path), Some(mode)) = ( item.path_opt(), item.file_metadata().map(|metadata| metadata.mode()), ) { permissions.push((path.clone(), set_mode_part(mode, shift, bits))); } } commands.push(Command::SetMultiplePermissions(permissions)); } }, Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; self.sort_direction = dir; if !matches!(self.location, Location::Desktop(..)) { commands.push(Command::SetSort( self.location.normalize().to_string(), heading_option, self.sort_direction, )); } } } Message::TabComplete(path, completions) => { if let Some(edit_location) = &mut self.edit_location && edit_location.location.path_opt() == Some(&path) { edit_location.completions = Some(completions); commands.push(Command::Iced( widget::text_input::focus(self.edit_location_id.clone()).into(), )); } } Message::Thumbnail(path, thumbnail) => { if let Some(ref mut items) = self.items_opt { let location = Location::Path(path); for item in items.iter_mut() { if item.location_opt.as_ref() == Some(&location) { let handle_opt = match &thumbnail { ItemThumbnail::NotImage => None, ItemThumbnail::Image(handle, _) => Some(widget::icon::Handle { symbolic: false, data: widget::icon::Data::Image(handle.clone()), }), ItemThumbnail::Svg(handle) => Some(widget::icon::Handle { symbolic: false, data: widget::icon::Data::Svg(handle.clone()), }), //TODO: text thumbnails? ItemThumbnail::Text(_text) => None, }; if let Some(handle) = handle_opt { item.icon_handle_grid.clone_from(&handle); item.icon_handle_list.clone_from(&handle); item.icon_handle_list_condensed = handle; } item.thumbnail_opt = Some(thumbnail); break; } } } } Message::ImageDecoded(path, width, height, pixels, display_size, generation) => { // Create handle from pre-decoded RGBA data (fast!) let handle = widget::image::Handle::from_rgba(width, height, pixels); // Store decoded image handle if generation still matches (not superseded) self.large_image_manager.store_decoded_with_generation( path, handle, display_size, generation, ); } Message::ToggleSort(heading_option) => { if !matches!(self.location, Location::Search(..)) { let heading_sort = if self.sort_name == heading_option { !self.sort_direction } else { // Default modified to descending, and others to ascending. heading_option != HeadingOptions::Modified }; if !matches!(self.location, Location::Desktop(..)) { commands.push(Command::SetSort( self.location.normalize().to_string(), heading_option, heading_sort, )); } self.sort_direction = heading_sort; self.sort_name = heading_option; } } Message::Drop(Some((to, mut from))) => { self.dnd_hovered = None; match to { Location::Desktop(to, ..) | Location::Path(to) | Location::Network(_, _, Some(to)) => { if let Ok(entries) = fs::read_dir(&to) { for i in entries.into_iter().filter_map(Result::ok) { let i = i.path(); from.paths.retain(|p| &i != p); if from.paths.is_empty() { log::info!("All dropped files already in target directory."); return commands; } } } commands.push(Command::DropFiles(to, from)); } Location::Trash if matches!(from.kind, ClipboardKind::Cut { .. }) => { commands.push(Command::Delete(from.paths)); } _ => { log::warn!("{:?} to {:?} is not supported.", from.kind, to); } } } Message::Drop(None) => { self.dnd_hovered = None; } Message::DndHover(loc) => { if self .dnd_hovered .as_ref() .is_some_and(|(l, i)| *l == loc && i.elapsed() > HOVER_DURATION) { cd = Some(loc); } } Message::DndEnter(loc) => { self.dnd_hovered = Some((loc.clone(), Instant::now())); if loc != self.location { commands.push(Command::Iced( cosmic::Task::future(async move { tokio::time::sleep(HOVER_DURATION).await; Message::DndHover(loc) }) .into(), )); } } Message::DndLeave(loc) => { if Some(&loc) == self.dnd_hovered.as_ref().map(|(l, _)| l) { self.dnd_hovered = None; } } Message::WindowDrag => { commands.push(Command::WindowDrag); } Message::WindowToggleMaximize => { commands.push(Command::WindowToggleMaximize); } Message::ZoomIn => { commands.push(Command::Action(Action::ZoomIn)); } Message::ZoomOut => { commands.push(Command::Action(Action::ZoomOut)); } Message::DirectorySize(path, dir_size) => { let location = Location::Path(path); if let Some(ref mut item) = self.parent_item_opt && item.location_opt.as_ref() == Some(&location) { item.dir_size.clone_from(&dir_size); } if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { if item.location_opt.as_ref() == Some(&location) { item.dir_size = dir_size; break; } } } } } // Scroll to top if needed if self.scroll_opt.is_none() { let offset = AbsoluteOffset { x: 0.0, y: 0.0 }; self.scroll_opt = Some(offset); commands.push(Command::Iced( scrollable::scroll_to( self.scrollable_id.clone(), AbsoluteOffset { x: Some(0.0), y: Some(0.0), }, ) .into(), )); } // Change directory if requested if let Some(mut location) = cd { location = location.normalize(); if matches!(self.mode, Mode::Desktop) { match location { Location::Path(path) => { commands.push(Command::OpenFile(vec![path])); } Location::Trash => { commands.push(Command::OpenTrash); } _ => {} } } else { // Select parent if location is not directory let mut selected_paths = None; if let Some(path) = location.path_opt() && !path.is_dir() && let Some(parent) = path.parent() { selected_paths = Some(vec![path.clone()]); location = location.with_path(parent.to_path_buf()); } if location != self.location || selected_paths.is_some() { if location.path_opt().is_none_or(|path| path.is_dir()) { if selected_paths.is_none() { selected_paths = self.location.path_opt().map(|path| vec![path.clone()]); } self.change_location(&location, history_i_opt); commands.push(Command::ChangeLocation( self.title(), location, selected_paths, )); } else { log::warn!("tried to cd to {location:?} which is not a directory"); } } } } // Update context menu popup if self.context_menu != last_context_menu { if last_context_menu.is_some() { commands.push(Command::ContextMenu(None, self.window_id)); } if let Some(point) = self.context_menu { commands.push(Command::ContextMenu(Some(point), self.window_id)); } } commands } pub(crate) const fn sort_options(&self) -> (HeadingOptions, bool, bool) { match self.location { Location::Search(..) => (HeadingOptions::Modified, false, false), _ => ( self.sort_name, self.sort_direction, self.config.folders_first, ), } } fn column_sort(&self) -> Option> { let check_reverse = |ord: Ordering, sort: bool| { if sort { ord } else { ord.reverse() } }; let mut items: Vec<_> = self.items_opt.as_ref()?.iter().enumerate().collect(); let (sort_name, sort_direction, folders_first) = self.sort_options(); match sort_name { HeadingOptions::Size => { items.sort_by(|a, b| { // entries take precedence over size let get_size = |x: &Item| match &x.metadata { ItemMetadata::Path { metadata, children_opt, } => { if metadata.is_dir() { (true, children_opt.unwrap_or_default() as u64) } else { (false, metadata.len()) } } ItemMetadata::Trash { metadata, .. } => match metadata.size { trash::TrashItemSize::Entries(entries) => (true, entries as u64), trash::TrashItemSize::Bytes(bytes) => (false, bytes), }, ItemMetadata::SimpleDir { entries } => (true, *entries), ItemMetadata::SimpleFile { size } => (false, *size), #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { size_opt, children_opt, .. } => match children_opt { Some(child_count) => (true, *child_count as u64), None => (false, size_opt.unwrap_or_default()), }, }; let (a_is_entry, a_size) = get_size(a.1); let (b_is_entry, b_size) = get_size(b.1); //TODO: use folders_first? match (a_is_entry, b_is_entry) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => check_reverse(a_size.cmp(&b_size), sort_direction), } }); } HeadingOptions::Name => items.sort_by(|a, b| { if folders_first { match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => check_reverse( LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name), sort_direction, ), } } else { check_reverse( LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name), sort_direction, ) } }), HeadingOptions::Modified => { items.sort_by(|a, b| { let a_modified = a.1.metadata.modified(); let b_modified = b.1.metadata.modified(); if folders_first { match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => check_reverse(a_modified.cmp(&b_modified), sort_direction), } } else { check_reverse(a_modified.cmp(&b_modified), sort_direction) } }); } HeadingOptions::TrashedOn => { let time_deleted = |x: &Item| match &x.metadata { ItemMetadata::Trash { entry, .. } => Some(entry.time_deleted), _ => None, }; items.sort_by(|a, b| { let a_time_deleted = time_deleted(a.1); let b_time_deleted = time_deleted(b.1); if folders_first { match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => check_reverse(a_time_deleted.cmp(&b_time_deleted), sort_direction), } } else { check_reverse(b_time_deleted.cmp(&a_time_deleted), sort_direction) } }); } } Some(items) } fn dnd_dest<'a>( &self, location: &Location, element: impl Into>, ) -> Element<'a, Message> { let location1 = location.clone(); let location2 = location.clone(); let location3 = location.clone(); let is_dnd_hovered = self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(location); let mut container = widget::container( DndDestination::for_data::(element, move |data, action| { if let Some(mut data) = data { if action == DndAction::Copy { Message::Drop(Some((location1.clone(), data))) } else if action == DndAction::Move { data.kind = ClipboardKind::Cut { is_dnd: true }; Message::Drop(Some((location1.clone(), data))) } else { log::warn!("unsupported action: {action:?}"); Message::Drop(None) } } else { Message::Drop(None) } }) .on_enter(move |_, _, _| Message::DndEnter(location2.clone())) .on_leave(move || Message::DndLeave(location3.clone())), ); // Desktop will not show DnD indicator if is_dnd_hovered && !matches!(self.mode, Mode::Desktop) { container = container.style(|t| { let mut a = widget::container::Style::default(); let t = t.cosmic(); // todo use theme drop target color let mut bg = t.accent_color(); bg.alpha = 0.2; a.background = Some(Color::from(bg).into()); a.border = Border { color: t.accent_color().into(), width: 1.0, radius: t.radius_s().into(), }; a }); } container.into() } pub fn gallery_view(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, space_xs, space_m, .. } = theme::spacing(); //TODO: display error messages when image not found? let mut name_opt = None; let mut element_opt: Option> = None; if let Some(index) = self.select_focus && let Some(items) = &self.items_opt && let Some(item) = items.get(index) { name_opt = Some(widget::text::heading(&item.display_name)); match item .thumbnail_opt .as_ref() .unwrap_or(&ItemThumbnail::NotImage) { ItemThumbnail::NotImage => {} ItemThumbnail::Image(handle, original_dims) => { // Determine which image to show based on async decode state let mut is_loading = false; let mut error_msg_opt = None; let image_handle = if let Some(path) = item.path_opt() { if let Some(error_msg) = self.large_image_manager.get_error(path) { error_msg_opt = Some(error_msg.clone()); handle.clone() } else if self.large_image_manager.is_decoding(path) { // Currently decoding (initial or re-decode) --> show cached/thumbnail with loading indicator is_loading = true; // Use decoded handle if available (re-decode), otherwise thumbnail (initial decode) self.large_image_manager .get_decoded(path) .cloned() .unwrap_or_else(|| handle.clone()) } else if let Some(decoded_handle) = self.large_image_manager.get_decoded(path) { // Decoded and not currently decoding --> use it decoded_handle.clone() } else if let Some((w, h)) = original_dims { // Check if image needs tiling if should_use_tiling(*w, *h) { // Large image --> show thumbnail only handle.clone() } else { // Normal-sized image --> load full resolution directly widget::image::Handle::from_path(path) } } else { // No dimensions available --> show thumbnail handle.clone() } } else { handle.clone() }; let content: cosmic::Element<'_, Message> = if let Some(error_msg) = error_msg_opt { widget::column::with_capacity(2) .push(widget::image(image_handle)) .push(widget::text(format!("⚠ {}", error_msg)).size(13)) .padding(space_xs) .align_x(cosmic::iced::Alignment::Center) .into() } else if is_loading { widget::column::with_capacity(2) .push(widget::image(image_handle)) .push(widget::text("Loading higher resolution...").size(14)) .padding(space_xs) .align_x(cosmic::iced::Alignment::Center) .into() } else { //TODO: use widget::image::viewer, when its zoom can be reset crate::load_image::loaded_image(image_handle).into() }; element_opt = Some(widget::container(content).center(Length::Fill).into()); } ItemThumbnail::Svg(handle) => { element_opt = Some( widget::svg(handle.clone()) .width(Length::Fill) .height(Length::Fill) .into(), ); } ItemThumbnail::Text(text) => { element_opt = Some( widget::container(widget::text_editor(text).padding(space_xxs).class( cosmic::theme::iced::TextEditor::Custom(Box::new(text_editor_class)), )) .center(Length::Fill) .into(), ); } } } let mut column = widget::column::with_capacity(2); column = column.push(widget::space::vertical().height(Length::Fixed(space_m.into()))); { let mut row = widget::row::with_capacity(5).align_y(Alignment::Center); row = row.push(widget::space::horizontal()); if let Some(name) = name_opt { row = row.push(name); } row = row.push(widget::space::horizontal()); row = row.push( widget::button::icon(widget::icon::from_name("window-close-symbolic")) .class(theme::Button::Standard) .on_press(Message::Gallery(false)), ); row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into()))); // This mouse area provides window drag while the header bar is hidden let mouse_area = mouse_area::MouseArea::new(row) .on_press(|_| Message::WindowDrag) .on_double_click(|_| Message::WindowToggleMaximize); column = column.push(mouse_area); } { let mut row = widget::row::with_capacity(7).align_y(Alignment::Center); row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into()))); row = row.push( widget::button::icon(widget::icon::from_name("go-previous-symbolic")) .padding(space_xs) .class(theme::Button::Standard) .on_press(Message::GalleryPrevious), ); row = row.push(widget::space::horizontal().width(Length::Fixed(space_xxs.into()))); if let Some(element) = element_opt { row = row.push(element); } else { //TODO: what to do when no image? row = row.push(space::horizontal().width(Length::Fill)); row = row.push(space::vertical().height(Length::Fill)); } row = row.push(widget::space::horizontal().width(Length::Fixed(space_xxs.into()))); row = row.push( widget::button::icon(widget::icon::from_name("go-next-symbolic")) .padding(space_xs) .class(theme::Button::Standard) .on_press(Message::GalleryNext), ); row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into()))); column = column.push(row); } widget::container(column) .width(Length::Fill) .height(Length::Fill) .style(|theme| { let cosmic = theme.cosmic(); let mut bg = cosmic.bg_color(); bg.alpha = 0.75; widget::container::Style { background: Some(Color::from(bg).into()), ..Default::default() } }) .into() } pub fn location_view(&self) -> Element<'_, Message> { //TODO: responsiveness is done in a hacky way, potentially move this to a custom widget? fn text_width<'a>( content: &'a str, font: font::Font, font_size: f32, line_height: f32, ) -> f32 { let text: text::Text<&'a str, font::Font> = text::Text { content, bounds: Size::INFINITE, size: font_size.into(), line_height: text::LineHeight::Absolute(line_height.into()), font, align_x: text::Alignment::Left, align_y: Vertical::Top, shaping: text::Shaping::default(), wrapping: text::Wrapping::None, ellipsize: text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1)), }; graphics::text::Paragraph::with_text(text) .min_bounds() .width } fn text_width_body(content: &str) -> f32 { //TODO: should libcosmic set the font when using widget::text::body? text_width(content, font::default(), 14.0, 20.0) } fn text_width_heading(content: &str) -> f32 { text_width(content, font::semibold(), 14.0, 20.0) } let cosmic_theme::Spacing { space_xxxs, space_xxs, space_s, space_m, .. } = theme::spacing(); let size = self.size_opt.get().unwrap_or(Size::new(0.0, 0.0)); let mut row = widget::row::with_capacity(5) .align_y(Alignment::Center) .padding([space_xxxs, 0]); let mut w = 0.0; let mut prev_button = widget::button::custom(widget::icon::from_name("go-previous-symbolic").size(16)) .padding(space_xxs) .class(theme::Button::Icon); if self.history_i > 0 && !self.history.is_empty() { prev_button = prev_button.on_press(Message::GoPrevious); } row = row.push(prev_button); w += f32::from(space_xxs).mul_add(2.0, 16.0); let mut next_button = widget::button::custom(widget::icon::from_name("go-next-symbolic").size(16)) .padding(space_xxs) .class(theme::Button::Icon); if self.history_i + 1 < self.history.len() { next_button = next_button.on_press(Message::GoNext); } row = row.push(next_button); w += f32::from(space_xxs).mul_add(2.0, 16.0); row = row.push(widget::space::horizontal().width(Length::Fixed(space_s.into()))); w += f32::from(space_s); //TODO: allow resizing? let name_width = 300.0; let modified_width = 200.0; let size_width = 100.0; let condensed = size.width < (name_width + modified_width + size_width); let (sort_name, sort_direction, _) = self.sort_options(); let heading_item = |name, width, msg| { let mut row = widget::row::with_capacity(2) .align_y(Alignment::Center) .spacing(space_xxxs) .width(width); row = row.push(widget::text::heading(name)); match (sort_name == msg, sort_direction) { (true, true) => { row = row.push(widget::icon::from_name("pan-down-symbolic").size(16)); } (true, false) => { row = row.push(widget::icon::from_name("pan-up-symbolic").size(16)); } _ => {} } //TODO: make it possible to resize with the mouse mouse_area::MouseArea::new(row) .on_press(move |_point_opt| Message::ToggleSort(msg)) .into() }; let heading_row = widget::row::with_children([ heading_item(fl!("name"), Length::Fill, HeadingOptions::Name), if self.location.is_trash() { heading_item( fl!("trashed-on"), Length::Fixed(modified_width), HeadingOptions::TrashedOn, ) } else { heading_item( fl!("modified"), Length::Fixed(modified_width), HeadingOptions::Modified, ) }, heading_item(fl!("size"), Length::Fixed(size_width), HeadingOptions::Size), ]) .align_y(Alignment::Center) .height(Length::Fixed((space_m + 4).into())) .padding([0, space_xxs]); let accent_rule = rule::horizontal(1).class(theme::Rule::Custom(Box::new(|theme| rule::Style { color: theme.cosmic().accent_color().into(), radius: 0.0.into(), fill_mode: rule::FillMode::Full, snap: true, }))); let heading_rule = widget::container(rule::horizontal(1)) .padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]); if let Some(edit_location) = &self.edit_location { let mut text_input = None; //TODO: allow editing other locations if let Location::Network(ref uri, ..) = edit_location.location { let location = edit_location.location.clone(); text_input = Some( widget::text_input("", uri.clone()) .id(self.edit_location_id.clone()) .on_input(move |input| { Message::EditLocation(Some(location.with_uri(input).into())) }) .on_submit(|_| Message::EditLocationSubmit) .line_height(1.0), ); } else if let Some(resolved_location) = edit_location.resolve() && let Some(path) = resolved_location.path_opt().cloned() { text_input = Some( widget::text_input("", path.to_string_lossy().into_owned()) .id(self.edit_location_id.clone()) .on_input(move |input| { Message::EditLocation(Some( resolved_location.with_path(PathBuf::from(input)).into(), )) }) .on_submit(|_| Message::EditLocationSubmit) .on_tab(Message::EditLocationTab) .on_unfocus(Message::EditLocation(None)) .line_height(1.0), ); } if let Some(text_input) = text_input { row = row.push( widget::button::custom( widget::icon::from_name("window-close-symbolic").size(16), ) .on_press(Message::EditLocation(None)) .padding(space_xxs) .class(theme::Button::Icon), ); let mut popover = widget::popover(text_input).position(widget::popover::Position::Bottom); if let Some(completions) = &edit_location.completions && !completions.is_empty() { let mut column = widget::column::with_capacity(completions.len()).padding(space_xxs); for (i, (name, _path)) in completions.iter().enumerate() { let selected = edit_location.selected == Some(i); column = column.push( widget::button::custom(widget::text::body(name)) //TODO: match to design .class(if selected { theme::Button::Standard } else { theme::Button::HeaderBar }) .on_press(Message::EditLocationComplete(i)) .padding(space_xxs) .width(Length::Fill), ); } popover = popover.popup( widget::container(column) .class(theme::Container::Dropdown) //TODO: This is a hack to get the popover to be the right width .max_width(size.width - 140.0), ); } row = row.push(popover); let mut column = widget::column::with_capacity(4).padding([0, space_s]); column = column.push(row); column = column.push(accent_rule); if self.config.view == View::List && !condensed { column = column.push(heading_row); column = column.push(heading_rule); } return column.into(); } } else if let Some(path) = self.location.path_opt() { row = row.push( crate::mouse_area::MouseArea::new( widget::button::custom(widget::icon::from_name("edit-symbolic").size(16)) .padding(space_xxs) .class(theme::Button::Icon) .on_press(Message::EditLocation(Some(self.location.clone().into()))), ) .on_middle_press(move |_| Message::OpenInNewTab(path.clone())), ); w += f32::from(space_xxs).mul_add(2.0, 16.0); } let mut children: Vec> = Vec::new(); match &self.location { Location::Desktop(path, ..) | Location::Path(path) | Location::Search(SearchLocation::Path(path), ..) => { let excess_str = "..."; let excess_width = text_width_body(excess_str); for (index, ancestor) in path.ancestors().enumerate() { let (name, found_home) = folder_name(ancestor); let (name_width, name_text) = if children.is_empty() { ( text_width_heading(&name), widget::text::heading(name) .wrapping(text::Wrapping::None) .ellipsize(text::Ellipsize::End( text::EllipsizeHeightLimit::Lines(1), )), ) } else { children.push( widget::icon::from_name("go-next-symbolic") .size(16) .icon() .into(), ); w += 16.0; ( text_width_body(&name), widget::text::body(name).wrapping(text::Wrapping::None), ) }; // Add padding for mouse area w += 2.0 * f32::from(space_xxxs); let mut row = widget::row::with_capacity(2) .align_y(Alignment::Center) .spacing(space_xxxs); //TODO: figure out why this hardcoded offset is needed after the first item is ellipsed let overflow_offset = 64.0; let overflow = w + name_width + overflow_offset > size.width && index > 0; if overflow { row = row.push(widget::text::body(excess_str)); w += excess_width; } else { row = row.push(name_text); w += name_width; } let location = self.location.with_path(ancestor.to_path_buf()); let mut mouse_area = crate::mouse_area::MouseArea::new( widget::button::custom(row) .padding(space_xxxs) .class(theme::Button::Link) .on_press(if ancestor == path { Message::EditLocation(Some(self.location.clone().into())) } else { Message::Location(location.clone()) }), ); if self.location_context_menu_index.is_some() { mouse_area = mouse_area .on_right_press(move |point_opt| { Message::LocationContextMenuIndex(point_opt, None) }) .wayland_on_right_press_window_position(); } else { mouse_area = mouse_area .on_right_press_no_capture() .on_right_press(move |point_opt| { Message::LocationContextMenuIndex(point_opt, Some(index)) }) .wayland_on_right_press_window_position(); } let mouse_area = if let Location::Path(_) = &self.location { mouse_area .on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf())) } else { mouse_area }; children.push(self.dnd_dest(&location, mouse_area)); if found_home || overflow { break; } } children.reverse(); } Location::Trash | Location::Search(SearchLocation::Trash, ..) => { children.push( widget::button::custom(widget::text::heading(fl!("trash"))) .padding(space_xxxs) .on_press(Message::Location(Location::Trash)) .class(theme::Button::Text) .into(), ); } Location::Recents | Location::Search(SearchLocation::Recents, ..) => { children.push( widget::button::custom(widget::text::heading(fl!("recents"))) .padding(space_xxxs) .on_press(Message::Location(Location::Recents)) .class(theme::Button::Text) .into(), ); } Location::Network(uri, display_name, path) => { children.push( widget::button::custom(widget::text::heading(display_name)) .padding(space_xxxs) .on_press(Message::Location(Location::Network( uri.clone(), display_name.clone(), path.clone(), ))) .class(theme::Button::Text) .into(), ); } } row = row.extend(children); let mut column = widget::column::with_capacity(4).padding([0, space_s]); column = column.push(row); column = column.push(accent_rule); if self.config.view == View::List && !condensed { column = column.push(heading_row); column = column.push(heading_rule); } let mouse_area = crate::mouse_area::MouseArea::new(column) .on_right_press(Message::LocationContextMenuPoint); let mut popover = widget::popover(mouse_area); if let (Some(point), Some(index)) = ( self.location_context_menu_point, self.location_context_menu_index, ) { popover = popover .popup(menu::location_context_menu(index)) .position(widget::popover::Position::Point(point)); } popover.into() } pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> { let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing(); mouse_area::MouseArea::new(widget::column::with_children([widget::container( match self.mode { Mode::App | Mode::Dialog(_) => widget::column::with_children([ widget::icon::from_name("folder-symbolic") .size(64) .icon() .into(), widget::text::body(if has_hidden { fl!("empty-folder-hidden") } else if matches!(self.location, Location::Search(..)) { fl!("no-results") } else { fl!("empty-folder") }) .into(), ]), Mode::Desktop => widget::column::with_capacity(0), } .align_x(Alignment::Center) .spacing(space_xxs), ) .center(Length::Fill) .into()])) .on_press(|_| Message::Click(None)) .into() } pub fn grid_view( &self, ) -> ( Option>, Element<'_, Message>, bool, ) { let cosmic_theme::Spacing { space_xxs, space_xxxs, .. } = theme::spacing(); let TabConfig { show_hidden, mut icon_sizes, .. } = self.config; let mut grid_spacing = space_xxs; if let Location::Desktop(_path, _output, desktop_config) = &self.location { icon_sizes.grid = desktop_config.icon_size; grid_spacing = desktop_config.grid_spacing_for(space_xxs); } let text_height = 3 * 20; // 3 lines of text let item_width = (3 * space_xxs + icon_sizes.grid() + 3 * space_xxs) as usize; let item_height = (space_xxxs + icon_sizes.grid() + space_xxxs + text_height + space_xxxs) as usize; let (width, height) = match self.size_opt.get() { Some(size) => ( (size.width.floor() as usize) .saturating_sub(2 * (space_xxs as usize)) .max(item_width), (size.height.floor() as usize).max(item_height), ), None => (item_width, item_height), }; let (cols, column_spacing) = { let width_m1 = width.saturating_sub(item_width); let cols_m1 = width_m1 / (item_width + grid_spacing as usize); let cols = cols_m1 + 1; let spacing = width_m1 .checked_div(cols_m1) .unwrap_or(0) .saturating_sub(item_width); (cols, spacing as u16) }; let rows = { let height_m1 = height.saturating_sub(item_height); let rows_m1 = height_m1 / (item_height + grid_spacing as usize); rows_m1 + 1 }; //TODO: move to function let visible_rect = { // Use cached content height to clamp scroll offset after resize let max_scroll_y = self .content_height_opt .get() .map(|ch| (ch - height as f32).max(0.0)) .unwrap_or(f32::MAX); let scroll_y = self .scroll_opt .map(|o| o.y.min(max_scroll_y).max(0.0)) .unwrap_or(0.0); let point = Point::new(0.0, scroll_y); let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0)); Rectangle::new(point, size) }; let mut grid = widget::grid() .column_spacing(column_spacing) .row_spacing(grid_spacing) .padding(space_xxs.into()); let mut dnd_items: Vec<(usize, (usize, usize), &Item)> = Vec::new(); let mut drag_w_i = usize::MAX; let mut drag_n_i = usize::MAX; let mut drag_e_i = 0; let mut drag_s_i = 0; let mut column = widget::column::with_capacity(2); if let Some(items) = self.column_sort() { let mut count = 0; let mut col = 0; let mut row = 0; let mut page_row = 0; let mut hidden = 0; let mut grid_elements = Vec::new(); for &(i, item) in &items { if !show_hidden && item.hidden { item.pos_opt.set(None); item.rect_opt.set(None); hidden += 1; continue; } item.pos_opt.set(Some((row, col))); let item_rect = Rectangle::new( Point::new( (col * (item_width + column_spacing as usize) + space_xxs as usize) as f32, (row * (item_height + grid_spacing as usize) + space_xxs as usize) as f32, ), Size::new(item_width as f32, item_height as f32), ); item.rect_opt.set(Some(item_rect)); //TODO: error if the row or col is already set? while grid_elements.len() <= row { grid_elements.push(Vec::new()); } // Only build elements if visible (for performance) if item_rect.intersects(&visible_rect) { //TODO: one focus group per grid item (needs custom widget) let buttons: Vec> = vec![ widget::button::custom( widget::icon::icon(item.icon_handle_grid.clone()) .content_fit(ContentFit::Contain) .size(icon_sizes.grid()) .width(Length::Shrink), ) .padding(space_xxxs) .class(button_style( item.selected, item.highlighted, item.cut, false, false, false, )) .into(), widget::tooltip( widget::button::custom(Item::grid_display_name(&item.display_name)) .id(item.button_id.clone()) .padding([0, space_xxxs]) .class(button_style( item.selected, item.highlighted, item.cut, true, true, matches!(self.mode, Mode::Desktop), )), widget::text::body(&item.name), widget::tooltip::Position::Bottom, ) .into(), ]; let mut column = widget::column::with_capacity(buttons.len()) .align_x(Alignment::Center) .height(Length::Fixed(item_height as f32)) .width(Length::Fixed(item_width as f32)); for button in buttons { if self.context_menu.is_some() { column = column.push(button); } else { column = column.push( mouse_area::MouseArea::new(button) .on_right_press_no_capture() .wayland_on_right_press_window_position() .on_right_press(move |point_opt| { Message::RightClick(point_opt, Some(i)) }), ); } } let column: Element = if item.metadata.is_dir() && item.location_opt.is_some() { self.dnd_dest(&item.location_opt.clone().unwrap(), column) } else { column.into() }; if item.selected { dnd_items.push((i, (row, col), item)); drag_w_i = drag_w_i.min(col); drag_n_i = drag_n_i.min(row); drag_e_i = drag_e_i.max(col); drag_s_i = drag_s_i.max(row); } let mouse_area = crate::mouse_area::MouseArea::new(column) .on_press(move |_| Message::Click(Some(i))) .on_double_click(move |_| Message::DoubleClick(Some(i))) .on_release(move |_| Message::ClickRelease(Some(i))) .on_middle_press(move |_| Message::MiddleClick(i)) .on_enter(move || Message::HighlightActivate(i)) .on_exit(move || Message::HighlightDeactivate(i)); grid_elements[row].push(Element::from(mouse_area)); } else { // Add a spacer if the row is empty, so scroll works if grid_elements[row].is_empty() { grid_elements[row].push(Element::from( widget::column::with_capacity(0) .width(Length::Fill) .height(Length::Fixed(item_height as f32)), )); } } count += 1; if matches!(self.mode, Mode::Desktop) { row += 1; if row >= page_row + rows { row = 0; col += 1; } if col >= cols { col = 0; page_row += rows; row = page_row; } } else { col += 1; if col >= cols { col = 0; row += 1; } } } for row_elements in grid_elements { for element in row_elements { grid = grid.push(element); } grid = grid.insert_row(); } if count == 0 { return (None, self.empty_view(hidden > 0), false); } column = column.push(grid); //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that { let mut max_bottom = 0; for (_, item) in items { if let Some(rect) = item.rect_opt.get() { let bottom = (rect.y + rect.height).ceil() as usize; if bottom > max_bottom { max_bottom = bottom; } } } // Cache content height for scroll clamping on next frame self.content_height_opt.set(Some(max_bottom as f32)); let top_deduct = 7 * (space_xxs as usize); self.item_view_size_opt .set(self.size_opt.get().map(|s| Size { width: s.width, height: s.height - top_deduct as f32, })); let spacer_height = height.saturating_sub(max_bottom + top_deduct); if spacer_height > 0 { column = column.push(widget::container( space::vertical().height(Length::Fixed(spacer_height as f32)), )); } } } let drag_list = (!dnd_items.is_empty()).then(|| { let mut dnd_grid = widget::grid() .column_spacing(column_spacing) .row_spacing(grid_spacing) .padding(space_xxs.into()); let mut dnd_item_i = 0; for r in drag_n_i..=drag_s_i { dnd_grid = dnd_grid.insert_row(); for c in drag_w_i..=drag_e_i { let Some((i, (row, col), item)) = dnd_items.get(dnd_item_i) else { break; }; if *row == r && *col == c { let buttons = vec![ widget::button::custom( widget::icon::icon(item.icon_handle_grid.clone()) .content_fit(ContentFit::Contain) .size(icon_sizes.grid()), ) .on_press(Message::Click(Some(*i))) .padding(space_xxxs) .class(button_style( item.selected, item.highlighted, item.cut, false, false, false, )), widget::button::custom(Item::grid_display_name( item.display_name.clone(), )) .id(item.button_id.clone()) .on_press(Message::Click(Some(*i))) .padding([0, space_xxxs]) .class(button_style( item.selected, item.highlighted, item.cut, true, true, false, )), ]; let column = widget::column::with_children(buttons.into_iter().map(Element::from)) .align_x(Alignment::Center) .height(Length::Fixed(item_height as f32)) .width(Length::Fixed(item_width as f32)); dnd_grid = dnd_grid.push(column); dnd_item_i += 1; } else { dnd_grid = dnd_grid.push( widget::container(space::vertical().height(item_width as f32)) .height(Length::Fixed(item_height as f32)), ); } } } Element::from(dnd_grid) }); let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill)) .on_press(|_| Message::Click(None)) .on_auto_scroll(Message::AutoScroll) .on_drag_end(|_| Message::DragEnd) .show_drag_rect(self.mode.multiple()) .on_release(|_| Message::ClickRelease(None)); if self.watch_drag { mouse_area = mouse_area.on_drag(Message::Drag); } (drag_list, mouse_area.into(), true) } pub fn list_view( &self, ) -> ( Option>, Element<'_, Message>, bool, ) { let cosmic_theme::Spacing { space_s, space_xxs, .. } = theme::spacing(); let TabConfig { show_hidden, icon_sizes, .. } = self.config; let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0)); //TODO: allow resizing? let name_width = 300.0; let modified_width = 200.0; let size_width = 100.0; let condensed = size.width < (name_width + modified_width + size_width); let is_search = matches!(self.location, Location::Search(..)); let icon_size = if condensed || is_search { icon_sizes.list_condensed() } else { icon_sizes.list() }; let row_height = icon_size + 2 * space_xxs; let mut column = widget::column::with_capacity(3); let mut y: f32 = 0.0; let rule_padding = theme::active().cosmic().corner_radii.radius_xs[0] as u16; //TODO: move to function let visible_rect = { // Use cached content height to clamp scroll offset after resize let max_scroll_y = self .content_height_opt .get() .map(|ch| (ch - size.height).max(0.0)) .unwrap_or(f32::MAX); let scroll_y = self .scroll_opt .map(|o| o.y.min(max_scroll_y).max(0.0)) .unwrap_or(0.0); let point = Point::new(0.0, scroll_y); let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0)); Rectangle::new(point, size) }; let mut drag_items = Vec::new(); if let Some(items) = self.column_sort() { let mut count = 0; let mut hidden = 0; for (i, item) in items { if item.hidden && !show_hidden { item.pos_opt.set(None); item.rect_opt.set(None); hidden += 1; continue; } if count > 0 { column = column .push(widget::container(rule::horizontal(1)).padding([0, rule_padding])); y += 1.0; } item.pos_opt.set(Some((count, 0))); let item_rect = Rectangle::new( Point::new(f32::from(space_s), y), Size::new(size.width - f32::from(2 * space_s), f32::from(row_height)), ); item.rect_opt.set(Some(item_rect)); // Only build elements if visible (for performance) let button_row = if item_rect.intersects(&visible_rect) { let modified_text = match &item.metadata { ItemMetadata::Path { metadata, .. } => match metadata.modified() { Ok(time) => self.format_time(time).to_string(), Err(_) => String::new(), }, ItemMetadata::Trash { entry, .. } => FormatTime::from_secs( entry.time_deleted, &self.date_time_formatter, &self.time_formatter, ) .map(|t| t.to_string()) .unwrap_or_default(), #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { .. } => match item.metadata.modified() { Some(mtime) => self.format_time(mtime).to_string(), None => String::new(), }, _ => String::new(), }; let size_text = match &item.metadata { ItemMetadata::Path { metadata, children_opt, } => { if metadata.is_dir() { //TODO: translate if let Some(children) = children_opt { if *children == 1 { format!("{children} item") } else { format!("{children} items") } } else { String::new() } } else { format_size(metadata.len()) } } ItemMetadata::Trash { metadata, .. } => match metadata.size { trash::TrashItemSize::Entries(entries) => { //TODO: translate if entries == 1 { format!("{entries} item") } else { format!("{entries} items") } } trash::TrashItemSize::Bytes(bytes) => format_size(bytes), }, ItemMetadata::SimpleDir { entries } => { //TODO: translate if *entries == 1 { format!("{entries} item") } else { format!("{entries} items") } } ItemMetadata::SimpleFile { size } => format_size(*size), #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { size_opt, children_opt, .. } => match children_opt { Some(child_count) => { if *child_count == 1 { format!("{child_count} item") } else { format!("{child_count} items") } } None => format_size(size_opt.unwrap_or_default()), }, }; let row = if condensed { widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), widget::column::with_children([ Item::list_display_name(item.display_name.clone()).into(), //TODO: translate? widget::text::caption(format!("{modified_text} - {size_text}")) .into(), ]) .into(), ]) .height(Length::Fixed(f32::from(row_height))) .align_y(Alignment::Center) .spacing(space_xxs) } else if is_search { widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), widget::column::with_children([ Item::list_display_name(item.display_name.clone()).into(), widget::text::caption(match item.path_opt() { Some(path) => path.display().to_string(), None => String::new(), }) .into(), ]) .width(Length::Fill) .into(), widget::text::body(modified_text.clone()) .width(Length::Fixed(modified_width)) .into(), widget::text::body(size_text.clone()) .width(Length::Fixed(size_width)) .into(), ]) .height(Length::Fixed(f32::from(row_height))) .align_y(Alignment::Center) .spacing(space_xxs) } else { widget::row::with_children([ widget::icon::icon(item.icon_handle_list.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), Item::list_display_name(item.display_name.clone()) .width(Length::Fill) .into(), widget::text::body(modified_text.clone()) .width(Length::Fixed(modified_width)) .into(), widget::text::body(size_text.clone()) .width(Length::Fixed(size_width)) .into(), ]) .height(Length::Fixed(f32::from(row_height))) .align_y(Alignment::Center) .spacing(space_xxs) }; let button = |row| { let mouse_area = crate::mouse_area::MouseArea::new( widget::button::custom(row) .width(Length::Fill) .id(item.button_id.clone()) .padding([0, space_xxs]) .class(button_style( item.selected, item.highlighted, item.cut, true, true, false, )), ) .on_press(move |_| Message::Click(Some(i))) .on_double_click(move |_| Message::DoubleClick(Some(i))) .on_release(move |_| Message::ClickRelease(Some(i))) .on_middle_press(move |_| Message::MiddleClick(i)) .on_enter(move || Message::HighlightActivate(i)) .on_exit(move || Message::HighlightDeactivate(i)); if self.context_menu.is_some() { mouse_area } else { mouse_area .on_right_press_no_capture() .wayland_on_right_press_window_position() .on_right_press(move |point_opt| { Message::RightClick(point_opt, Some(i)) }) } }; let button_row = button(row.into()); let button_row: Element<_> = if item.metadata.is_dir() && let Some(location) = item.location_opt.as_ref() { self.dnd_dest(location, button_row) } else { button_row.into() }; if item.selected || !drag_items.is_empty() { let dnd_row = if !item.selected { Element::from( space::vertical().height(Length::Fixed(f32::from(row_height))), ) } else if condensed { widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), widget::column::with_children([ Item::list_display_name(item.display_name.clone()).into(), //TODO: translate? widget::text::body(format!("{modified_text} - {size_text}")) .into(), ]) .into(), ]) .align_y(Alignment::Center) .spacing(space_xxs) .into() } else if is_search { widget::row::with_children([ widget::icon::icon(item.icon_handle_list_condensed.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), widget::column::with_children([ Item::list_display_name(item.display_name.clone()).into(), widget::text::caption(match item.path_opt() { Some(path) => path.display().to_string(), None => String::new(), }) .into(), ]) .width(Length::Fill) .into(), widget::text::body(modified_text.clone()) .width(Length::Fixed(modified_width)) .into(), widget::text::body(size_text.clone()) .width(Length::Fixed(size_width)) .into(), ]) .align_y(Alignment::Center) .spacing(space_xxs) .into() } else { widget::row::with_children([ widget::icon::icon(item.icon_handle_list.clone()) .content_fit(ContentFit::Contain) .size(icon_size) .into(), Item::list_display_name(item.display_name.clone()) .width(Length::Fill) .into(), widget::text(modified_text) .width(Length::Fixed(modified_width)) .into(), widget::text::body(size_text) .width(Length::Fixed(size_width)) .into(), ]) .align_y(Alignment::Center) .spacing(space_xxs) .into() }; if item.selected { drag_items.push( widget::container(button(dnd_row)) .width(Length::Shrink) .into(), ); } else { drag_items.push(dnd_row); } } button_row } else { widget::column::with_capacity(0) .width(Length::Fill) .height(Length::Fixed(f32::from(row_height))) .into() }; count += 1; y += f32::from(row_height); column = column.push(button_row); } if count == 0 { return (None, self.empty_view(hidden > 0), false); } // Cache content height for scroll clamping on next frame self.content_height_opt.set(Some(y)); } //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that { let top_deduct = (if condensed || is_search { 6 } else { 9 }) * space_xxs; self.item_view_size_opt .set(self.size_opt.get().map(|s| Size { width: s.width, height: s.height - f32::from(top_deduct), })); let spacer_height = size.height - y - f32::from(top_deduct); if spacer_height > 0. { column = column.push(widget::container(space::vertical().height(spacer_height))); } } let drag_col = (!drag_items.is_empty()) .then(|| Element::from(widget::column::with_children(drag_items))); let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s])) .with_id(Id::new("list-view")) .on_press(|_| Message::Click(None)) .on_auto_scroll(Message::AutoScroll) .on_drag_end(|_| Message::DragEnd) .show_drag_rect(self.mode.multiple()) .on_release(|_| Message::ClickRelease(None)); if self.watch_drag { mouse_area = mouse_area.on_drag(Message::Drag); } (drag_col, mouse_area.into(), true) } pub fn view_responsive<'a>( &'a self, key_binds: &'a HashMap, modifiers: &'a Modifiers, size: Size, clipboard_paste_available: bool, context_actions: &'a [ContextActionPreset], ) -> Element<'a, Message> { // Update cached size self.size_opt.set(Some(size)); let cosmic_theme::Spacing { space_xxxs, space_xxs, space_xs, .. } = theme::spacing(); let location_view_opt = if matches!(self.mode, Mode::Desktop) { None } else { Some(self.location_view()) }; let (drag_list, mut item_view, can_scroll) = match self.config.view { View::Grid => self.grid_view(), View::List => self.list_view(), }; item_view = widget::container(item_view).width(Length::Fill).into(); let files = self .items_opt .as_ref() .map(|items| { items .iter() .filter_map(|item| { if item.selected { item.path_opt().cloned() } else { None } }) .collect::>() }) .unwrap_or_default(); let item_view = DndSource::::with_id(item_view, Id::new("tab-view")); let view = self.config.view; let item_view = match drag_list { Some(drag_list) if self.selected_clicked => { let drag_list = RcElementWrapper::new(drag_list); item_view .drag_content(move || { ClipboardCopy::new(crate::clipboard::ClipboardKind::Copy, &files) }) .drag_icon(move |_| { let state: tree::State = Widget::::state(&drag_list); ( Element::from(drag_list.clone()).map(|_m| ()), state, match view { // offset by grid padding so that we grab the top left corner of the item in the drag grid. View::Grid => Vector::new( f32::from(space_xxs).mul_add(-3.0, -f32::from(space_xxxs)), -4. * f32::from(space_xxxs), ), View::List => Vector::ZERO, }, ) }) } _ => item_view, }; let tab_location = self.location.clone(); let mouse_area = mouse_area::MouseArea::new(item_view) .on_press(move |_point_opt| Message::Click(None)) .on_release(|_| Message::ClickRelease(None)) .on_resize(Message::Resize) .on_back_press(move |_point_opt| Message::GoPrevious) .on_forward_press(move |_point_opt| Message::GoNext) .on_scroll(|delta| respond_to_scroll_direction(delta, modifiers)) .on_right_press(move |p| { Message::ContextMenu( if self.context_menu.is_some() { None } else { p }, self.window_id, ) }) .wayland_on_right_press_window_position(); let mut popover = widget::popover(mouse_area); if let Some(point) = self.context_menu && (!cfg!(feature = "wayland") || !crate::is_wayland()) { let context_menu = menu::context_menu( self, key_binds, modifiers, clipboard_paste_available, context_actions, ); popover = popover .popup(context_menu) .position(widget::popover::Position::Point(point)); } let mut tab_column = widget::column::with_capacity(3); if let Some(location_view) = location_view_opt { tab_column = tab_column.push(location_view); } if can_scroll { tab_column = tab_column.push( // FIXME: new responsive widget will remove the state from the scrollable // id_container with custom id forces the state to be extracted in a diff // pre-processing step widget::id_container( widget::scrollable(popover) .id(self.scrollable_id.clone()) .on_scroll(Message::Scroll) .width(Length::Fill) .height(Length::Fill), widget::Id::new(format!("{}-scrollable", self.scrollable_id)), ), ); } else { tab_column = tab_column.push(popover); } match &self.location { Location::Trash | Location::Search(SearchLocation::Trash, ..) => { if let Some(items) = self.items_opt() && !items.is_empty() { tab_column = tab_column.push( widget::layer_container(widget::row::with_children([ widget::space::horizontal().into(), widget::button::standard(fl!("empty-trash")) .on_press(Message::EmptyTrash) .into(), ])) .padding([space_xxs, space_xs]) .layer(cosmic_theme::Layer::Primary) .apply(widget::container) .padding([0, 0, 7, 0]), ); } } Location::Network(uri, _display_name, _path) if uri == "network:///" => { tab_column = tab_column.push( widget::layer_container(widget::row::with_children([ widget::space::horizontal().into(), widget::button::standard(fl!("add-network-drive")) .on_press(Message::AddNetworkDrive) .into(), ])) .padding([space_xxs, space_xs]) .layer(cosmic_theme::Layer::Primary) .apply(widget::container) .padding([0, 0, 7, 0]), ); } _ => {} } let mut tab_view = widget::container(tab_column) .height(Length::Fill) .width(Length::Fill); // Desktop will not show DnD indicator if self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location) && !matches!(self.mode, Mode::Desktop) { tab_view = tab_view.style(|t| { let mut a = widget::container::Style::default(); let c = t.cosmic(); a.border = cosmic::iced::core::Border { color: (c.accent_color()).into(), width: 1., radius: c.radius_0().into(), }; a }); } let tab_location_2 = self.location.clone(); let tab_location_3 = self.location.clone(); let dnd_dest = DndDestination::for_data(tab_view, move |data, action| { if let Some(mut data) = data { if action == DndAction::Copy { Message::Drop(Some((tab_location.clone(), data))) } else if action == DndAction::Move { data.kind = ClipboardKind::Cut { is_dnd: true }; Message::Drop(Some((tab_location.clone(), data))) } else { log::warn!("unsupported action: {action:?}"); Message::Drop(None) } } else { Message::Drop(None) } }) .on_enter(move |_, _, _| Message::DndEnter(tab_location_2.clone())) .on_leave(move || Message::DndLeave(tab_location_3.clone())); dnd_dest.into() } pub fn multi_preview_view<'a>( &'a self, mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, space_m, .. } = theme::spacing(); let mut column = widget::column::with_capacity(4).spacing(space_m); let handle = widget::icon::from_name("text-x-generic") .size(IconSizes::default().grid()) .handle(); let icon = widget::icon::icon(handle.clone()) .content_fit(ContentFit::Contain) .size(IconSizes::default().grid()); let icon_container1 = widget::container(icon.clone()).padding(padding::bottom(10).left(10)); let icon_container2 = widget::container(icon.clone()).padding(padding::top(5).bottom(5).left(5).right(5)); let icon_container3 = widget::container(icon).padding(padding::top(10).right(10)); let stack = stack![icon_container1, icon_container2, icon_container3]; column = column.push( widget::container(stack) .center_x(Length::Fill) .max_height(THUMBNAIL_SIZE as f32), ); let selected_items: Vec<&Item> = self.items_opt().map_or(Vec::new(), |items| { items .iter() .filter(|item| { if item.selected { item.location_opt .as_ref() .and_then(Location::path_opt) .is_some() } else { false } }) .collect() }); let mut details = widget::column::with_capacity(3).spacing(space_xxxs); details = details.push(widget::text::body(fl!( "items", items = selected_items.len() ))); let mut total_size: u64 = 0; let mut mime_type_counts: BTreeMap = BTreeMap::new(); let mut user_name: BTreeSet = BTreeSet::new(); let mut mode_user: BTreeSet = BTreeSet::new(); let mut group_name: BTreeSet = BTreeSet::new(); let mut mode_group: BTreeSet = BTreeSet::new(); let mut mode_other: BTreeSet = BTreeSet::new(); let mut calculating_dir_size = false; let mut dir_size_error: Option = None; for item in selected_items.iter() { *mime_type_counts.entry(item.mime.to_string()).or_insert(0) += 1; if let Some(metadata) = item.file_metadata() { if metadata.is_dir() { match &item.dir_size { DirSize::Calculating(_) => { calculating_dir_size = true; } DirSize::Directory(size) => { total_size = total_size.saturating_add(*size); } DirSize::NotDirectory => (), DirSize::Error(err) => { dir_size_error = Some(err.clone()); } }; } else { total_size = total_size.saturating_add(metadata.len()); } #[cfg(unix)] { let mode = metadata.mode(); user_name.insert( uzers::get_user_by_uid(metadata.uid()) .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(), ); mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER)); group_name.insert( uzers::get_group_by_gid(metadata.gid()) .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) .unwrap_or_default(), ); mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP)); mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER)); } } } let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect(); mime_types.sort_by(|(_, v1), (_, v2)| v2.cmp(v1)); // Limit the number of displayed mime types let limit = usize::min(10, mime_types.len()); let mut mime_type_strings: Vec = mime_types[..limit] .iter() .map(|(mime, count)| format!("{} ({})", mime, count)) .collect(); if mime_types.len() > limit { mime_type_strings.push("...".to_string()); } details = details.push(widget::text::body(fl!( "type", mime = mime_type_strings.join(", ") ))); let size = { if calculating_dir_size { fl!("calculating") } else if let Some(error) = dir_size_error { error } else { format_size(total_size) } }; details = details.push(widget::text::body(fl!("item-size", size = size))); column = column.push(details); column = column.push(widget::button::standard(fl!("open")).on_press(Message::Open(None))); let mut settings = Vec::new(); // Only allow modifying open-with if all mime types are the same if mime_types.len() == 1 && let Some(mime) = mime_types .first() .and_then(|(mime, _)| mime.parse::().ok()) && let Some(mime_app_cache) = mime_app_cache_opt { let mime_apps = mime_app_cache.get(&mime); if !mime_apps.is_empty() { let mime_closure = mime.clone(); settings.push( widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( mime_apps, mime_apps.iter().position(|x| x.is_default), move |index| (index, mime_closure.clone()), ) .icons(Cow::Borrowed(mime_app_cache.icons(&mime))), ) .map(|(index, mime)| { let mime_app = &mime_apps[index]; Message::SetOpenWith(mime, mime_app.id.clone()) }), ), ); } } #[cfg(unix)] { // Only return mode part if it's the only one fn selected_mode_part(mut modes: BTreeSet) -> Option { match (modes.pop_first(), modes.pop_first()) { (Some(mode), None) => Some(mode.try_into().unwrap()), _ => None, } } // Convert a limited number of values from a set into a comma separated list fn join_set(set: BTreeSet) -> String { let limit = 5; let mut title = set.into_iter().collect::>(); if title.len() > limit { title.truncate(limit); title.push("...".to_string()); } title.join(", ") } let mode_part_user = selected_mode_part(mode_user); settings.push( widget::settings::item::builder(join_set(user_name)) .description(fl!("owner")) .control( widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), mode_part_user, move |selected| { Message::ShiftPermissions( None, MODE_SHIFT_USER, selected.try_into().unwrap(), ) }, ) .placeholder(fl!("mixed")), ), ); let mode_part_group = selected_mode_part(mode_group); settings.push( widget::settings::item::builder(join_set(group_name)) .description(fl!("group")) .control( widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), mode_part_group, move |selected| { Message::ShiftPermissions( None, MODE_SHIFT_GROUP, selected.try_into().unwrap(), ) }, ) .placeholder(fl!("mixed")), ), ); let mode_part_other = selected_mode_part(mode_other); settings.push( widget::settings::item::builder(fl!("other")).control( widget::dropdown( Cow::Borrowed(MODE_NAMES.as_slice()), mode_part_other, move |selected| { Message::ShiftPermissions( None, MODE_SHIFT_OTHER, selected.try_into().unwrap(), ) }, ) .placeholder(fl!("mixed")), ), ); } if !settings.is_empty() { let mut section = widget::settings::section(); section = section.extend(settings); column = column.push(section); } column.into() } pub fn view<'a>( &'a self, key_binds: &'a HashMap, modifiers: &'a Modifiers, clipboard_paste_available: bool, context_actions: &'a [ContextActionPreset], ) -> Element<'a, Message> { widget::responsive(move |size| { widget::id_container( self.view_responsive( key_binds, modifiers, size, clipboard_paste_available, context_actions, ), Id::new(format!( "tab-{}-{}", self.scrollable_id, self.location_title )), ) .into() }) .into() } pub fn subscription(&self, preview: bool) -> Subscription { //TODO: how many thumbnail loads should be in flight at once? let jobs = self.thumb_config.jobs.get() as usize; let mut subscriptions = Vec::with_capacity(jobs + 3); if let Some(items) = &self.items_opt { //TODO: move to function let visible_rect = { let point = match self.scroll_opt { Some(offset) => Point::new(0.0, offset.y), None => Point::new(0.0, 0.0), }; let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0)); Rectangle::new(point, size) }; for item in items { if item.thumbnail_opt.is_some() { // Skip items that already have a mime type and thumbnail continue; } match item.rect_opt.get() { Some(rect) => { if !rect.intersects(&visible_rect) { // Skip items that are not visible continue; } } None => { // Skip items with no determined rect (this should include hidden items) continue; } } let Some(path) = item.path_opt().cloned() else { continue; }; let metadata = item.metadata.clone(); let can_thumbnail = match metadata { ItemMetadata::Path { .. } => true, #[cfg(feature = "gvfs")] ItemMetadata::GvfsPath { .. } => true, _ => false, }; if can_thumbnail { let mime = item.mime.clone(); let max_jobs = jobs; let max_mb = u64::from(self.thumb_config.max_mem_mb.get()); let max_size = u64::from(self.thumb_config.max_size_mb.get()); // Determine effective memory budget based on image size let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE { match item.image_dimensions { Some((width, height)) => { let (_use_dedicated, eff_mb, eff_jobs) = should_use_dedicated_worker(width, height, max_mb, max_jobs); (eff_mb, eff_jobs) } None => (max_mb, max_jobs), } } else { (max_mb, max_jobs) }; #[derive(Clone)] struct Wrapper { path: PathBuf, metadata: ItemMetadata, mime: mime::Mime, effective_max_mb: u64, effective_jobs: usize, max_size: u64, } impl Hash for Wrapper { fn hash(&self, state: &mut H) { self.path.hash(state); } } subscriptions.push(Subscription::run_with( Wrapper { path: path.clone(), metadata, mime, effective_max_mb, effective_jobs, max_size, }, |wrapper| { let Wrapper { path, metadata, mime, effective_max_mb, effective_jobs, max_size, } = wrapper.clone(); stream::channel( 1, move |mut output: futures::channel::mpsc::Sender<_>| async move { while crate::operation::is_actively_writing_to(&path) { crate::operation::actively_writing_tick().await; } let message = { let path = path.clone(); // Acquire semaphore permit _ = THUMB_SEMAPHORE.acquire().await; tokio::task::spawn_blocking(move || { let start = Instant::now(); let thumbnail = ItemThumbnail::new( &path, metadata, mime, THUMBNAIL_SIZE, effective_max_mb, effective_jobs, max_size, ); log::debug!( "thumbnailed {} in {:?}", path.display(), start.elapsed() ); Message::Thumbnail(path, thumbnail) }) .await .unwrap() }; match output.send(message).await { Ok(()) => {} Err(err) => { log::warn!( "failed to send thumbnail for {}: {}", path.display(), err ); } } std::future::pending().await }, ) }, )); } if subscriptions.len() >= jobs { break; } } if preview { // Load directory size for selected items let mut selected_items: Vec<&Item> = items.iter().filter(|item| item.selected).collect(); if selected_items.is_empty() && let Some(p) = self.parent_item_opt.as_ref() { selected_items.push(p) } for item in selected_items { // Item must have a path if let Some(path) = item.path_opt().cloned() { // Item must be calculating directory size if let DirSize::Calculating(controller) = &item.dir_size { struct Wrapper { path: PathBuf, controller: Controller, } impl Hash for Wrapper { fn hash(&self, state: &mut H) { self.path.hash(state); } } subscriptions.push(Subscription::run_with( Wrapper { path: path.clone(), controller: controller.clone() }, |Wrapper { path, controller }| { let path = path.clone(); let controller = controller.clone(); stream::channel(1, |mut output: futures::channel::mpsc::Sender<_>| async move { let message = { let start = Instant::now(); match calculate_dir_size(&path, controller).await { Ok(size) => { log::debug!( "calculated directory size of {} in {:?}", path.display(), start.elapsed() ); Message::DirectorySize( path.clone(), DirSize::Directory(size), ) } Err(err) => { log::warn!( "failed to calculate directory size of {}: {}", path.display(), err ); Message::DirectorySize( path.clone(), DirSize::Error(err.to_string()), ) } } }; match output.send(message).await { Ok(()) => {} Err(err) => { log::warn!( "failed to send directory size for {}: {}", path.display(), err ); } } std::future::pending().await }) } )); } } } } } // Load search items incrementally if let Location::Search(search_location, term, show_hidden, start) = &self.location { let location = self.location.clone(); let search_location = search_location.clone(); let term = term.clone(); let show_hidden = *show_hidden; let start = *start; #[derive(Debug, Hash, Clone)] struct Wrapper { location: Location, search_location: SearchLocation, term: String, show_hidden: bool, start: Instant, } subscriptions.push(Subscription::run_with( Wrapper { location: location.clone(), search_location: search_location.clone(), term: term.clone(), show_hidden, start, }, |wrapper| { let wrapper = wrapper.clone(); stream::channel( 2, move |mut output: futures::channel::mpsc::Sender| async move { let Wrapper { location, search_location, term, show_hidden, start, } = wrapper; //TODO: optimal size? let (results_tx, results_rx) = mpsc::channel(65536); let ready = Arc::new(atomic::AtomicBool::new(false)); let last_modified_opt = Arc::new(RwLock::new(None)); output .send(Message::SearchContext( location.clone(), SearchContextWrapper(Some(SearchContext { results_rx, ready: ready.clone(), last_modified_opt: last_modified_opt.clone(), })), )) .await .unwrap(); let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true); { tokio::task::spawn_blocking(move || { scan_search( &search_location, &term, show_hidden, move |search_item| -> bool { // Don't send if the result is too old if let Some(last_modified) = *last_modified_opt.read().unwrap() && let SearchItem::Path(_, _, ref metadata) = search_item { if let Ok(modified) = metadata.modified() { if modified < last_modified { return true; } } else { return true; } } match results_tx.blocking_send(search_item) { Ok(()) => { if ready.swap(true, atomic::Ordering::SeqCst) { true } else { // Wake up update method watch_tx.send(false).is_ok() } } Err(_) => false, } }, ); log::info!( "searched for {:?} in {} in {:?}", term, search_location, start.elapsed(), ); }); } while watch_rx.changed().await.is_ok() { let is_ready = *watch_rx.borrow_and_update(); let _ = output.send(Message::SearchReady(is_ready)).await; } // Send final ready let _ = output.send(Message::SearchReady(true)).await; std::future::pending().await }, ) }, )); } if let Some(path) = self .edit_location .as_ref() .and_then(|x| x.location.path_opt()) .cloned() { subscriptions.push(Subscription::run_with( ("tab_complete", path.clone()), |(_, path)| { let path = path.clone(); stream::channel( 1, |mut output: futures::channel::mpsc::Sender<_>| async move { let message = { let path = path.clone(); tokio::task::spawn_blocking(move || { let start = Instant::now(); match tab_complete(&path) { Ok(completions) => { log::info!( "tab completed {} in {:?}", path.display(), start.elapsed() ); Message::TabComplete(path.clone(), completions) } Err(err) => { log::warn!( "failed to tab complete {}: {}", path.display(), err ); Message::TabComplete(path.clone(), Vec::new()) } } }) .await .unwrap() }; match output.send(message).await { Ok(()) => {} Err(err) => { log::warn!( "failed to send tab completion for {}: {}", path.display(), err ); } } std::future::pending().await }, ) }, )); } Subscription::batch(subscriptions) } const fn format_time(&self, time: SystemTime) -> FormatTime<'_> { format_time(time, &self.date_time_formatter, &self.time_formatter) } } pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> Option { if !modifiers.control() { return None; } let delta_y = match delta { ScrollDelta::Lines { y, .. } => y, ScrollDelta::Pixels { y, .. } => y, }; if delta_y > 0.0 { return Some(Message::ZoomIn); } if delta_y < 0.0 { return Some(Message::ZoomOut); } None } fn text_editor_class( theme: &cosmic::Theme, status: cosmic::widget::text_editor::Status, ) -> cosmic::iced::widget::text_editor::Style { let cosmic = theme.cosmic(); let container = theme.current_container(); let mut background: cosmic::iced::Color = container.component.base.into(); background.a = 0.25; let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let mut placeholder = cosmic.palette.neutral_9; placeholder.alpha = 0.7; let placeholder = placeholder.into(); match status { cosmic::iced::widget::text_editor::Status::Active | cosmic::iced::widget::text_editor::Status::Disabled => { cosmic::iced::widget::text_editor::Style { background: background.into(), border: cosmic::iced::Border { radius: cosmic.corner_radii.radius_m.into(), width: 2.0, color: container.component.divider.into(), }, placeholder, value, selection, } } cosmic::iced::widget::text_editor::Status::Hovered | cosmic::iced::widget::text_editor::Status::Focused { .. } => { cosmic::iced::widget::text_editor::Style { background: background.into(), border: cosmic::iced::Border { radius: cosmic.corner_radii.radius_m.into(), width: 2.0, color: cosmic::iced::Color::from(cosmic.accent.base), }, placeholder, value, selection, } } } } #[cfg(test)] mod tests { use std::path::PathBuf; use std::{fs, io}; use cosmic::iced::mouse::ScrollDelta; use cosmic::iced::runtime::keyboard::Modifiers; use cosmic::widget; use log::{debug, trace}; use tempfile::TempDir; use test_log::test; use super::{Location, Message, Tab, respond_to_scroll_direction, scan_path}; use crate::app::test_utils::{ NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, assert_eq_tab_path, empty_fs, eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new, }; use crate::config::{IconSizes, TabConfig, ThumbCfg}; // Boilerplate for tab tests. Checks if simulated clicks selected items. fn tab_selects_item( clicks: &[usize], modifiers: Modifiers, expected_selected: &[bool], ) -> io::Result<()> { let (_fs, mut tab) = tab_click_new(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; // Simulate clicks by triggering Message::Click for &click in clicks { debug!("Emitting Message::Click(Some({click})) with modifiers: {modifiers:?}"); tab.update(Message::Click(Some(click)), modifiers); } let items = tab .items_opt .as_deref() .expect("tab should be populated with items"); for (i, (&expected, actual)) in expected_selected.iter().zip(items).enumerate() { assert_eq!( expected, actual.selected, "expected index {i} to be {}", if expected { "selected but it was deselected" } else { "deselected but it was selected" } ); } Ok(()) } fn tab_history() -> io::Result<(TempDir, Tab, Vec)> { let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); let mut tab = Tab::new( Location::Path(path.into()), TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); // All directories (simple_fs only produces one nested layer) let dirs: Vec = { let top_level = filter_dirs(path)?; let mut result = Vec::new(); for dir in top_level { let nested_dirs = filter_dirs(&dir)?; result.push(dir); result.extend(nested_dirs); } result }; assert!( dirs.len() == NUM_DIRS + NUM_DIRS * NUM_NESTED, "Sanity check: Have {} dirs instead of {}", dirs.len(), NUM_DIRS + NUM_DIRS * NUM_NESTED ); debug!("Building history by emitting Message::Location"); for dir in &dirs { debug!( "Emitting Message::Location(Location::Path(\"{}\"))", dir.display() ); tab.update( Message::Location(Location::Path(dir.clone())), Modifiers::empty(), ); } trace!("Tab history: {:?}", tab.history); Ok((fs, tab, dirs)) } #[test] fn scan_path_succeeds_on_valid_path() -> io::Result<()> { let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); // Read directory entries and sort as cosmic-files does let entries = read_dir_sorted(path)?; debug!("Calling scan_path(\"{}\")", path.display()); let actual = scan_path(&path.to_owned(), IconSizes::default()); // scan_path shouldn't skip any entries assert_eq!(entries.len(), actual.len()); // Correct files should be scanned assert!( entries .into_iter() .zip(actual.into_iter()) .all(|(path, item)| eq_path_item(&path, &item)) ); Ok(()) } #[test] fn scan_path_returns_empty_vec_for_invalid_path() -> io::Result<()> { let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); // A nonexisting path within the temp dir let invalid_path = path.join("ferris"); assert!(!invalid_path.exists()); debug!("Calling scan_path(\"{}\")", invalid_path.display()); let actual = scan_path(&invalid_path, IconSizes::default()); assert!(actual.is_empty()); Ok(()) } #[test] fn scan_path_empty_dir_returns_empty_vec() -> io::Result<()> { let fs = empty_fs()?; let path = fs.path(); debug!("Calling scan_path(\"{}\")", path.display()); let actual = scan_path(&path.to_owned(), IconSizes::default()); assert_eq!(0, path.read_dir()?.count()); assert!(actual.is_empty()); Ok(()) } #[test] fn tab_location_changes_location() -> io::Result<()> { let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); // Next directory in temp directory // This does not have to be sorted let next_dir = filter_dirs(path)? .next() .expect("temp directory should have at least one directory"); let mut tab = Tab::new( Location::Path(path.to_owned()), TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); debug!( "Emitting Message::Location(Location::Path(\"{}\"))", next_dir.display() ); tab.update( Message::Location(Location::Path(next_dir.clone())), Modifiers::empty(), ); // Validate that the tab's path updated // NOTE: `items_opt` is set to None with Message::Location so this ONLY checks for equal paths // If item contents are NOT None then this needs to be reevaluated for correctness assert_eq_tab_path(&tab, &next_dir); assert!( tab.items_opt.is_none(), "Tab's `items` is not None which means this test needs to be updated" ); Ok(()) } #[test] fn tab_click_single_selects_item() -> io::Result<()> { // Select the second directory with no keys held down tab_selects_item(&[1], Modifiers::empty(), &[false, true]) } #[test] fn tab_click_double_opens_folder() -> io::Result<()> { let (fs, mut tab) = tab_click_new(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?; let path = fs.path(); // Simulate double clicking second directory debug!("Emitting double click Message::DoubleClick(Some(1))"); tab.update(Message::DoubleClick(Some(1)), Modifiers::empty()); // Path to second directory let second_dir = read_dir_sorted(path)? .into_iter() .filter(|p| p.is_dir()) .nth(1) .expect("should be at least two directories"); // Location should have changed to second_dir assert_eq_tab_path(&tab, &second_dir); Ok(()) } #[test] fn tab_click_ctrl_selects_multiple() -> io::Result<()> { // Select the first and second directory by holding down ctrl tab_selects_item(&[0, 1], Modifiers::CTRL, &[true, true]) } #[test] fn tab_gonext_moves_forward_in_history() -> io::Result<()> { let (fs, mut tab, dirs) = tab_history()?; let path = fs.path(); // Rewind to the start for _ in 0..dirs.len() { debug!("Emitting Message::GoPrevious to rewind to the start",); tab.update(Message::GoPrevious, Modifiers::empty()); } assert_eq_tab_path(&tab, path); // Back to the future. Directories should be in the order they were opened. for dir in dirs { debug!("Emitting Message::GoNext",); tab.update(Message::GoNext, Modifiers::empty()); assert_eq_tab_path(&tab, &dir); } Ok(()) } #[test] fn tab_goprev_moves_backward_in_history() -> io::Result<()> { let (fs, mut tab, dirs) = tab_history()?; let path = fs.path(); for dir in dirs.into_iter().rev() { assert_eq_tab_path(&tab, &dir); debug!("Emitting Message::GoPrevious",); tab.update(Message::GoPrevious, Modifiers::empty()); } assert_eq_tab_path(&tab, path); Ok(()) } #[test] fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> { let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, &Modifiers::CTRL); assert!(message_maybe.is_some()); assert!(matches!(message_maybe.unwrap(), Message::ZoomIn)); Ok(()) } #[test] fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { let message_maybe = respond_to_scroll_direction( ScrollDelta::Pixels { x: 0.0, y: 1.0 }, &Modifiers::empty(), ); assert!(message_maybe.is_none()); Ok(()) } #[test] fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> { let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, &Modifiers::CTRL); assert!(message_maybe.is_some()); assert!(matches!(message_maybe.unwrap(), Message::ZoomOut)); Ok(()) } #[test] fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { let message_maybe = respond_to_scroll_direction( ScrollDelta::Pixels { x: 0.0, y: -1.0 }, &Modifiers::empty(), ); assert!(message_maybe.is_none()); Ok(()) } #[test] fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); let mut tab = Tab::new( Location::Path(path.into()), TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); // Tab's location shouldn't change if GoPrev or GoNext is triggered debug!("Emitting Message::GoPrevious",); tab.update(Message::GoPrevious, Modifiers::empty()); assert_eq_tab_path(&tab, path); debug!("Emitting Message::GoNext",); tab.update(Message::GoNext, Modifiers::empty()); assert_eq_tab_path(&tab, path); Ok(()) } #[test] fn tab_locationup_moves_up_hierarchy() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); let mut next_dir = filter_dirs(path)? .next() .expect("should be at least one directory"); let mut tab = Tab::new( Location::Path(next_dir.clone()), TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); // This will eventually yield false once root is hit while next_dir.pop() { debug!("Emitting Message::LocationUp",); tab.update(Message::LocationUp, Modifiers::empty()); assert_eq_tab_path(&tab, &next_dir); } Ok(()) } #[test] fn sort_long_number_file_names() -> io::Result<()> { let fs = empty_fs()?; let path = fs.path(); // Create files with names 255 characters long that only contain a single number // Example: 0000...0 for 255 characters // https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations let mut base_nums: Vec<_> = ('0'..='9').collect(); fastrand::shuffle(&mut base_nums); debug!("Shuffled numbers for paths: {base_nums:?}"); let paths: Vec<_> = base_nums .iter() .copied() .map(|base| path.join(std::iter::repeat_n(base, 255).collect::())) .collect(); for (file, base) in paths.iter().zip(base_nums.into_iter()) { trace!("Creating long file name for {base}"); fs::File::create(file)?; } debug!("Creating tab for directory of long file names"); Tab::new( Location::Path(path.into()), TabConfig::default(), ThumbCfg::default(), None, widget::Id::unique(), None, ); Ok(()) } #[test] fn mode_calculations() { use super::{ MODE_SHIFT_GROUP, MODE_SHIFT_OTHER, MODE_SHIFT_USER, get_mode_part, set_mode_part, }; for user in 0..=7 { for group in 0..=7 { for other in 0..=7 { let mode = (user << MODE_SHIFT_USER) | (group << MODE_SHIFT_GROUP) | (other << MODE_SHIFT_OTHER); assert_eq!(format!("{mode:03o}"), format!("{user:o}{group:o}{other:o}"),); assert_eq!(get_mode_part(mode, MODE_SHIFT_USER), user); assert_eq!(get_mode_part(mode, MODE_SHIFT_GROUP), group); assert_eq!(get_mode_part(mode, MODE_SHIFT_OTHER), other); let mode_no_user = (group << MODE_SHIFT_GROUP) | (other << MODE_SHIFT_OTHER); assert_eq!( format!("{mode_no_user:03o}"), format!("0{group:o}{other:o}") ); assert_eq!(set_mode_part(mode_no_user, MODE_SHIFT_USER, user), mode); let mode_no_group = (user << MODE_SHIFT_USER) | (other << MODE_SHIFT_OTHER); assert_eq!( format!("{mode_no_group:03o}"), format!("{user:o}0{other:o}") ); assert_eq!(set_mode_part(mode_no_group, MODE_SHIFT_GROUP, group), mode); let mode_no_other = (user << MODE_SHIFT_USER) | (group << MODE_SHIFT_GROUP); assert_eq!( format!("{mode_no_other:03o}"), format!("{user:o}{group:o}0") ); assert_eq!(set_mode_part(mode_no_other, MODE_SHIFT_OTHER, other), mode); } } } } } ================================================ FILE: src/thumbnail_cacher.rs ================================================ use image::DynamicImage; use md5::{Digest, Md5}; use rustc_hash::FxHashMap; use std::error::Error; use std::fs::{self, File}; use std::io::{self, BufReader, BufWriter}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::time::UNIX_EPOCH; use tempfile::NamedTempFile; use url::Url; /// Implements thumbnail caching based on the freedesktop.org Thumbnail Managing Standard. /// / pub struct ThumbnailCacher { file_path: PathBuf, file_uri: String, thumbnail_dir: PathBuf, thumbnail_path: PathBuf, thumbnail_size: ThumbnailSize, thumbnail_fail_marker_path: PathBuf, } impl ThumbnailCacher { pub fn new(file_path: &Path, thumbnail_size: ThumbnailSize) -> Result { let file_uri = thumbnail_uri(file_path) .map_err(|err| format!("failed to create URI for {}: {}", file_path.display(), err))?; let cache_base_dir = THUMBNAIL_CACHE_BASE_DIR .as_ref() .ok_or("failed to get thumbnail cache directory".to_string())?; let thumbnail_filename = thumbnail_cache_filename(&file_uri); let thumbnail_dir = cache_base_dir.join(thumbnail_size.subdirectory_name()); if !thumbnail_dir.is_dir() { log::warn!( "{} is not a directory, creating one now", thumbnail_dir.display() ); let _: () = log::error!( "{} failed to create directory, this error can be expected on first run", thumbnail_dir.display() ); fs::create_dir_all(&thumbnail_dir).unwrap_or(()); } let thumbnail_path = thumbnail_dir.join(&thumbnail_filename); let thumbnail_fail_marker_path = cache_base_dir .join("fail") .join(format!("cosmic-files-{}", env!("CARGO_PKG_VERSION"))) .join(&thumbnail_filename); Ok(Self { file_path: file_path.to_path_buf(), file_uri, thumbnail_dir, thumbnail_path, thumbnail_size, thumbnail_fail_marker_path, }) } pub fn get_cached_thumbnail(&self) -> CachedThumbnail { // If the file is already a thumbnail, just use it so we don't generate // cached thumbnails of thumbnails. if let (Some(cache_base_dir), Ok(metadata)) = ( THUMBNAIL_CACHE_BASE_DIR.as_ref(), std::fs::metadata(&self.file_path), ) && metadata.is_file() && self.file_path.starts_with(cache_base_dir) { return CachedThumbnail::Valid((self.file_path.clone(), None)); } // Use cached thumbnail if it is valid. if self.is_thumbnail_valid(&self.thumbnail_path) { return CachedThumbnail::Valid(( self.thumbnail_path.clone(), Some(self.thumbnail_size), )); } // Check if there is a fail marker from an earlier failure. if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) { return CachedThumbnail::Failed; } CachedThumbnail::RequiresUpdate(self.thumbnail_size) } pub fn thumbnail_dir(&self) -> &Path { &self.thumbnail_dir } pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box> { #[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)?; Ok(&self.thumbnail_path) } pub fn update_with_image(&self, image: DynamicImage) -> Result<&Path, Box> { let temp_file = tempfile::Builder::new() .prefix("cosmic-files-") .tempfile_in(&self.thumbnail_dir)?; { let file = File::create(temp_file.path())?; let image = image .thumbnail( self.thumbnail_size.pixel_size(), self.thumbnail_size.pixel_size(), ) .into_rgba8(); let writer = BufWriter::new(file); let mut encoder = png::Encoder::new(writer, image.width(), image.height()); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); encoder .write_header()? .write_image_data(&image.into_raw())?; } self.update_with_temp_file(temp_file) } pub fn create_fail_marker(&self) -> Result<(), Box> { 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))?; } let file = File::create(&self.thumbnail_fail_marker_path)?; let writer = BufWriter::new(file); let mut encoder = png::Encoder::new(writer, 1, 1); encoder.set_color(png::ColorType::Grayscale); encoder.set_depth(png::BitDepth::One); encoder.write_header()?.write_image_data(&[0])?; self.update_thumbnail_text_metadata(&self.thumbnail_fail_marker_path) } fn update_thumbnail_text_metadata(&self, path: &Path) -> Result<(), Box> { let file = File::open(path)?; let reader = BufReader::new(file); let decoder = png::Decoder::new(reader); let mut reader = decoder.read_info()?; let (width, height, color_type, bit_depth, mut text_chunks) = { let info = reader.info(); let text_chunks: FxHashMap = info .uncompressed_latin1_text .iter() .map(|chunk| (chunk.keyword.clone(), chunk.text.clone())) .collect(); ( info.width, info.height, info.color_type, info.bit_depth, text_chunks, ) }; let mut image_data = vec![ 0; reader .output_buffer_size() .ok_or("The required image buffer size is too large.")? ]; reader.next_frame(&mut image_data)?; let file = File::create(path)?; let writer = BufWriter::new(file); let mut encoder = png::Encoder::new(writer, width, height); encoder.set_color(color_type); encoder.set_depth(bit_depth); text_chunks.insert("Software".to_string(), "COSMIC Files".to_string()); text_chunks.insert("Thumb::URI".to_string(), self.file_uri.clone()); let metadata = std::fs::metadata(&self.file_path)?; let size = metadata.len(); text_chunks.insert("Thumb::Size".to_string(), size.to_string()); let mtime = metadata .modified()? .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); text_chunks.insert("Thumb::MTime".to_string(), mtime.to_string()); for (keyword, text) in text_chunks { encoder.add_text_chunk(keyword, text)?; } let mut writer = encoder.write_header()?; writer.write_image_data(&image_data)?; Ok(()) } fn is_thumbnail_valid(&self, thumbnail_path: &Path) -> bool { let thumbnail_file = match File::open(thumbnail_path) { Ok(file) => file, Err(_) => return false, }; let decoder = png::Decoder::new(BufReader::new(thumbnail_file)); let reader = match decoder.read_info() { Ok(reader) => reader, Err(err) => { log::warn!( "failed to decode {} as PNG: {}", thumbnail_path.display(), err ); return false; } }; let texts = &reader.info().uncompressed_latin1_text; // Thumb::URI is required and must match. let thumb_uri = texts .iter() .find(|&text| text.keyword == "Thumb::URI") .map(|t| &t.text); if let Some(thumb_uri) = thumb_uri { if *thumb_uri != self.file_uri { return false; } } else { return false; } let metadata = match std::fs::metadata(&self.file_path) { Ok(m) => m, Err(err) => { log::warn!( "failed to get metatdata of {}: {}", self.file_path.display(), err ); return false; } }; // Thumb::MTime is required and must match. let thumb_mtime = texts .iter() .find(|&text| text.keyword == "Thumb::MTime") .map(|t| &t.text); if let Some(thumb_mtime) = thumb_mtime { let modified = match metadata.modified() { Ok(m) => m, Err(err) => { log::warn!( "failed to get modified from metatdata of {}, {}", self.file_path.display(), err ); return false; } }; let mtime = modified .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() .to_string(); if *thumb_mtime != mtime { return false; } } else { return false; } // Thumb::Size isn't required, but it should be verified if present. let thumb_size = texts .iter() .find(|&text| text.keyword == "Thumb::Size") .map(|t| &t.text); if let Some(thumb_size) = thumb_size { let size = metadata.len(); if *thumb_size != size.to_string() { return false; } } true } } fn thumbnail_uri(path: &Path) -> io::Result { let absolute_path = fs::canonicalize(path)?; let url = Url::from_file_path(&absolute_path).map_err(|()| { io::Error::other(format!( "failed to create URI for thumbnail_file: {}", absolute_path.display() )) })?; // Technically square brackets don't need to be percent encoded, // and they aren't by the url crate, but the thumbnailer used by // Gnome Files does. In order to share thumbnails and not get duplicates // we should do the same. let url = url.as_str().replace('[', "%5B").replace(']', "%5D"); Ok(url) } fn thumbnail_cache_filename(file_uri: &str) -> String { let hash = Md5::digest(file_uri); format!("{hash:x}.png") } #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u32)] pub enum ThumbnailSize { Normal = 128, Large = 256, XLarge = 512, XXLarge = 1024, } impl ThumbnailSize { pub fn from_pixel_size(pixel_size: u32) -> Self { if pixel_size <= Self::Normal.pixel_size() { Self::Normal } else if pixel_size <= Self::Large.pixel_size() { Self::Large } else if pixel_size <= Self::XLarge.pixel_size() { Self::XLarge } else { Self::XXLarge } } pub const fn pixel_size(self) -> u32 { self as u32 } pub const fn subdirectory_name(self) -> &'static str { match self { Self::Normal => "normal", Self::Large => "large", Self::XLarge => "x-large", Self::XXLarge => "xx-large", } } } pub enum CachedThumbnail { /// The cached thumbnail is valid and should be used with size if known. Valid((PathBuf, Option)), /// The cached thumbnail doesn't exist or it's invalid and /// needs to be recreated with the pixel size. RequiresUpdate(ThumbnailSize), // The cached thumbnail is in a failed state. // This means it failed to create by cosmic-files in the past // and shouldn't be tried again. Failed, } static THUMBNAIL_CACHE_BASE_DIR: LazyLock> = LazyLock::new(|| { if let Some(cache_dir) = dirs::cache_dir() { return Some(cache_dir.join("thumbnails")); } log::warn!("failed to get thumbnail cache directory, thumbnails will not be cached"); None }); ================================================ FILE: src/thumbnailer.rs ================================================ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only #[cfg(feature = "desktop")] use cosmic::desktop::fde::GenericEntry; use mime_guess::Mime; use rustc_hash::FxHashMap; use std::path::Path; use std::sync::{LazyLock, Mutex}; use std::time::Instant; use std::{fs, process}; #[derive(Clone, Debug)] pub struct Thumbnailer { pub exec: String, } impl Thumbnailer { pub fn command( &self, input: &Path, output: &Path, thumbnail_size: u32, ) -> Option { let args_vec: Vec = shlex::split(&self.exec)?; let mut args = args_vec.iter(); let mut command = process::Command::new(args.next()?); for arg in args { if arg.starts_with('%') { match arg.as_str() { "%i" | "%u" => { command.arg(input); } "%o" => { command.arg(output); } "%s" => { command.arg(format!("{thumbnail_size}")); } _ => { log::warn!( "unsupported thumbnailer Exec code {:?} in {:?}", arg, self.exec ); return None; } } } else { command.arg(arg); } } Some(command) } } pub struct ThumbnailerCache { cache: FxHashMap>, } impl ThumbnailerCache { pub fn new() -> Self { let mut thumbnailer_cache = Self { cache: FxHashMap::default(), }; thumbnailer_cache.reload(); thumbnailer_cache } #[cfg(not(feature = "desktop"))] pub fn reload(&mut self) {} #[cfg(feature = "desktop")] pub fn reload(&mut self) { let start = Instant::now(); self.cache.clear(); let mut search_dirs = Vec::new(); let xdg_dirs = xdg::BaseDirectories::new(); if let Some(mut data_home) = xdg_dirs.get_data_home() { data_home.push("thumbnailers"); search_dirs.push(data_home); } search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| { data_dir.push("thumbnailers"); data_dir })); let mut thumbnailer_paths = Vec::new(); for dir in search_dirs { log::trace!("looking for thumbnailers in {}", dir.display()); match fs::read_dir(&dir) { Ok(entries) => { thumbnailer_paths.extend(entries.filter_map(|entry_res| { entry_res .inspect_err(|err| { log::warn!( "failed to read entry in directory {}: {}", dir.display(), err ) }) .ok() .map(|entry| entry.path()) })); } Err(err) => { log::warn!("failed to read directory {}: {}", dir.display(), err); } } } //TODO: handle directory specific behavior for path in thumbnailer_paths { let entry = match GenericEntry::from_path(&path) { Ok(ok) => ok, Err(err) => { log::warn!("failed to parse {}: {}", path.display(), err); continue; } }; //TODO: use TryExec? let Some(section) = entry.group("Thumbnailer Entry") else { log::warn!( "missing Thumbnailer Entry section for thumbnailer {}", path.display() ); continue; }; let Some(exec) = section.entry("Exec") else { log::warn!("missing Exec attribute for thumbnailer {}", path.display()); continue; }; let Some(mime_types) = section.entry("MimeType") else { log::warn!( "missing MimeType attribute for thumbnailer {}", path.display() ); continue; }; for mime_type in mime_types.split_terminator(';') { if let Ok(mime) = mime_type.parse::() { log::trace!("thumbnailer {}={}", mime, path.display()); let apps = self .cache .entry(mime) .or_insert_with(|| Vec::with_capacity(1)); apps.push(Thumbnailer { exec: exec.to_string(), }); } } } let elapsed = start.elapsed(); log::info!("loaded thumbnailer cache in {elapsed:?}"); } pub fn get(&self, key: &Mime) -> Vec { self.cache.get(key).map_or_else(Vec::new, Vec::clone) } } static THUMBNAILER_CACHE: LazyLock> = LazyLock::new(|| Mutex::new(ThumbnailerCache::new())); pub fn thumbnailer(mime: &Mime) -> Vec { let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap(); thumbnailer_cache.get(mime) } ================================================ FILE: src/trash.rs ================================================ 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, trash::Error> { Err(trash::Error::Unknown { description: "reading trash folders not supported on this platform".into(), }) } fn scan(_sizes: IconSizes) -> Vec { log::warn!("viewing trash not supported on this platform"); Vec::new() } fn scan_search 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, trash::Error> { trash::os_limited::trash_folders() } fn scan(sizes: IconSizes) -> Vec { 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 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 {} ================================================ FILE: src/zoom.rs ================================================ use std::num::NonZeroU16; use crate::config::IconSizes; use crate::tab::View; static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap(); static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap(); static MAX_ZOOM: NonZeroU16 = NonZeroU16::new(500).unwrap(); const ZOOM_STEP: u16 = 25; pub(crate) const fn zoom_to_default(view: View, icon_sizes: &mut IconSizes) { let icon_size = select_resized_icon(view, icon_sizes); *icon_size = DEFAULT_ZOOM; } pub(crate) fn zoom_in_view(view: View, icon_sizes: &mut IconSizes) { let icon_size = select_resized_icon(view, icon_sizes); let mut step = MIN_ZOOM; while step <= MAX_ZOOM { if *icon_size < step { *icon_size = step; break; } step = step.saturating_add(ZOOM_STEP); } if *icon_size > step { *icon_size = step; } } pub(crate) fn zoom_out_view(view: View, icon_sizes: &mut IconSizes) { let icon_size = select_resized_icon(view, icon_sizes); let mut step = MAX_ZOOM; while step >= MIN_ZOOM { if *icon_size > step { *icon_size = step; break; } step = NonZeroU16::new(step.get().saturating_sub(ZOOM_STEP)).unwrap(); } if *icon_size < step { *icon_size = step; } } const fn select_resized_icon(view: View, icon_sizes: &mut IconSizes) -> &mut NonZeroU16 { match view { View::Grid => &mut icon_sizes.grid, View::List => &mut icon_sizes.list, } }