[
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nPlease include in the title whether this is a bug report or a feature request.\n\nAlso, please search existing issues (both open and closed) to make sure what you're reporting or requesting hasn't already been reported or requested.\n\nIf this is a question or general discussion topic, please start a conversation in our chat https://chat.pop-os.org in the ~cosmic-epoch channel.\n-->\n\n**Cosmic-files version:**\n<!-- (run `apt policy cosmic-files`, or the appropriate substitution for your package manager) -->\n\n**Issue/Bug description:**\n\n**Steps to reproduce:**\n\n**Expected behavior:**\n\n**Other notes:**\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "- [ ] I have disclosed use of any AI generated code in my commit messages.\n  - 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.\n  - 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.\n- [ ] I understand these changes in full and will be able to respond to review comments.\n- [ ] My change is accurately described in the commit message.\n- [ ] My contribution is tested and working as described.\n- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.\n\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Cargo Build & Test\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  test:\n    name: cosmic-files - latest\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - run: sudo apt-get update; sudo apt-get install libclang-dev libglib2.0-dev libxkbcommon-dev\n      - run: rustup update stable && rustup default stable\n      - run: cargo test --verbose --no-default-features\n      - run: cargo test --verbose\n      - run: cargo test --verbose --all-features\n"
  },
  {
    "path": ".github/workflows/validate-desktop-files.yml",
    "content": "name: Validate .desktop files\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    container:\n      image: ubuntu:25.10\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Install desktop-file-utils\n        run: |\n          apt-get update\n          apt-get install -y desktop-file-utils findutils\n\n      - name: Validate .desktop files\n        run: |\n          set -e\n          echo \"Checking for .desktop files...\"\n          files=$(find . -type f -name \"*.desktop\")\n          if [ -z \"$files\" ]; then\n            echo \"No .desktop files found.\"\n            exit 0\n          fi\n\n          echo \"$files\" | while read -r file; do\n            echo \"Validating: $file\"\n            desktop-file-validate \"$file\"\n          done\n"
  },
  {
    "path": ".gitignore",
    "content": "/.cargo/\n/debian/*debhelper*\n/debian/cosmic-files.substvars\n/debian/cosmic-files/\n/debian/files\n/flamegraph.svg\n/heaptrack.*\n/perf.*\n/target/\n/test/\n/vendor.tar\n/vendor/\n"
  },
  {
    "path": ".zed/settings.json",
    "content": "{\n  \"format_on_save\": \"on\",\n  \"lsp\": {\n    \"rust-analyzer\": {\n      \"initialization_options\": {\n        \"check\": {\n          \"command\": \"clippy\",\n        },\n        \"rustfmt\": {\n          \"extraArgs\": [\"+nightly\"],\n        },\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"cosmic-files\"\nversion = \"1.0.12\"\nauthors = [\"Jeremy Soller <jeremy@system76.com>\"]\nedition = \"2024\"\nlicense = \"GPL-3.0-only\"\nrust-version = \"1.93\"\n\n[dependencies]\nanyhow = \"1\"\njiff = \"0.2\"\njiff-icu = \"0.2\"\nicu = { version = \"2.2.0\", features = [\"compiled_data\"] }\ncctk = { git = \"https://github.com/pop-os/cosmic-protocols\", package = \"cosmic-client-toolkit\", rev = \"160b086\", optional = true }\ncosmic-mime-apps = { git = \"https://github.com/pop-os/cosmic-mime-apps.git\", optional = true }\ndirs = \"6.0.0\"\ngio = { version = \"0.21\", optional = true }\nglib = { version = \"0.21\", optional = true }\nglob = \"0.3\"\nignore = \"0.4\"\nimage = \"0.25\"\nlibc = \"0.2\"\nlog = \"0.4\"\nmime_guess = \"2\"\nnotify-debouncer-full = \"0.7\"\nnotify-rust = { version = \"4\", optional = true }\nopen = \"5.3.4\"\npaste = \"1.0\"\nregex = \"1\"\nrustc-hash = \"2.1\"\nserde = { version = \"1\", features = [\"serde_derive\"] }\nshlex = { version = \"1.3\" }\ntempfile = \"3\"\ntikv-jemallocator = { version = \"0.6\", optional = true }\ntokio = { version = \"1\", features = [\"process\", \"sync\"] }\ntrash = { git = \"https://github.com/jackpot51/trash-rs.git\", branch = \"cosmic\" }\nurl = \"2.5\"\nwalkdir = \"2.5.0\"\nwayland-client = { version = \"0.31.14\", optional = true }\nxdg = { version = \"3.0\", optional = true }\nxdg-mime = { git = \"https://github.com/ebassi/xdg-mime-rs\" }\n# Compression\nbzip2 = { version = \"0.6\", optional = true }           #TODO: replace with pure Rust crate\nflate2 = \"1.1\"\ntar = \"0.4.45\"\nlzma-rust2 = { version = \"0.16\", optional = true }\nordermap = { version = \"1.2.0\", features = [\"serde\"] }\n# Internationalization\ni18n-embed = { version = \"0.16\", features = [\n    \"fluent-system\",\n    \"desktop-requester\",\n] }\ni18n-embed-fl = \"0.10\"\nrust-embed = \"8\"\nslotmap = \"1.1.1\"\nrecently-used-xbel = \"1.2.0\"\nzip = \"8\"\nmd-5 = \"0.10.6\"\npng = \"0.18\"\njxl-oxide = { version = \"0.12.5\", features = [\"image\"] }\nnum_cpus = \"1.17.0\"\nfiletime = \"0.2\"\ntracing = \"0.1.44\"\ntracing-subscriber = { version = \"0.3.23\", features = [\"env-filter\"] }\nthiserror = \"2.0.18\"\natomic_float = \"1.1.0\"\nnum_enum = \"0.7.6\"\nbstr = \"1.12.1\"\n\n# Completion-based IO runtime to enable io_uring / IOCP file IO support.\n[dependencies.compio]\nversion = \"0.18\"\ndefault-features = false\nfeatures = [\"fs\", \"io\", \"macros\", \"polling\", \"runtime\"]\n\n[dependencies.libcosmic]\ngit = \"https://github.com/pop-os/libcosmic.git\"\ndefault-features = false\n#TODO: a11y feature crashes\nfeatures = [\n    \"about\",\n    \"advanced-shaping\",\n    \"autosize\",\n    \"multi-window\",\n    \"tokio\",\n    \"winit\",\n    \"surface-message\",\n]\n\n[[example]]\nname = \"gio-list\"\nrequired-features = [\"gvfs\"]\n\n[[example]]\nname = \"gio-mount\"\nrequired-features = [\"gvfs\"]\n\n[[example]]\nname = \"gvfs\"\nrequired-features = [\"gvfs\"]\n\n[features]\ndefault = [\n    \"bzip2\",\n    \"dbus-config\",\n    \"desktop\",\n    \"gvfs\",\n    \"io-uring\",\n    \"lzma-rust2\",\n    \"notify\",\n    \"wayland\",\n    \"wgpu\",\n]\ndbus-config = [\"libcosmic/dbus-config\"]\ndesktop = [\"libcosmic/desktop\", \"dep:cosmic-mime-apps\", \"dep:xdg\"]\ndesktop-applet = []\ngvfs = [\"dep:gio\", \"dep:glib\"]\nio-uring = [\"compio/io-uring\"]\njemalloc = [\"dep:tikv-jemallocator\"]\nnotify = [\"dep:notify-rust\"]\nwayland = [\"libcosmic/wayland\", \"dep:cctk\", \"dep:wayland-client\"]\nwgpu = [\"libcosmic/wgpu\"]\n\n[profile.dev]\nopt-level = 1\n\n[profile.release-with-debug]\ninherits = \"release\"\ndebug = true\n\n[target.'cfg(unix)'.dependencies]\nfork = \"0.7\"\nuzers = \"0.12.2\"\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\nprocfs = \"0.18\"\n\n[build-dependencies]\nxdgen = \"0.1\"\n\n[dev-dependencies]\n# cap-std = \"3\"\n# cap-tempfile = \"3\"\nfastrand = \"2\"\ntest-log = \"0.2\"\ntokio = { version = \"1\", features = [\"rt\", \"macros\"] }\n\n# [patch.'https://github.com/pop-os/cosmic-text']\n# cosmic-text = { path = \"../cosmic-text\" }\n\n# [patch.'https://github.com/pop-os/libcosmic']\n# libcosmic = { path = \"../libcosmic\" }\n# cosmic-config = { path = \"../libcosmic/cosmic-config\" }\n# cosmic-theme = { path = \"../libcosmic/cosmic-theme\" }\n# libcosmic = { git = \"https://github.com/pop-os/libcosmic//\", branch = \"iced-rebase\" }\n# cosmic-config = { git = \"https://github.com/pop-os/libcosmic//\", branch = \"iced-rebase\" }\n# cosmic-theme = { git = \"https://github.com/pop-os/libcosmic//\", branch = \"iced-rebase\" }\n\n\n# [patch.'https://github.com/pop-os/smithay-clipboard']\n# smithay-clipboard = { path = \"../smithay-clipboard\" }\n\n[workspace]\nmembers = [\"cosmic-files-applet\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# cosmic-files\nFile manager for the COSMIC desktop environment\n\n## Build the project from source\n\n```sh\n# Clone the project using `git`\ngit clone https://github.com/pop-os/cosmic-files\n# Change to the directory that was created by `git`\ncd cosmic-files\n# Build an optimized version using `cargo`, this may take a while\ncargo build --release\n# Run the optimized version using `cargo`\ncargo run --release\n```\n\n## Community and Contributing\n\nThe COSMIC desktop environment is maintained by System76 for use in Pop!_OS. A list of all COSMIC projects can be found in the\n[cosmic-epoch](https://github.com/pop-os/cosmic-epoch) project's README. If you would like to discuss COSMIC and Pop!_OS, please\nconsider joining the [Pop!_OS Chat](https://chat.pop-os.org/). More information and links can be found on the\n[Pop!_OS Website](https://pop.system76.com).\n\n## License\n\nThis project is licensed under [GPLv3](LICENSE)\n"
  },
  {
    "path": "TESTING.md",
    "content": "# Testing\n\nThis document provides a regression testing checklist for COSMIC Files. The checklist provides a starting point for Quality Assurance reviews.\n\n## Checklist\n\n### Basic navigation\n\n- [ ] Middle-click opens directory in a new tab (not focused).\n- [ ] Open two scrollable tabs. Scroll one tab, then switch to the other tab; it should not have scrolled.\n- [ ] Hover over the top item in the folder, then scroll down until it's out of view (while still hovered).\n      On scrolling back up (with the mouse in a different position), the item should not have the hover highlight.\n- [ ] Right-click an item in the sidebar. No visual change should occur with the rest of the items.\n- [ ] Remove an item from the sidebar, then re-pin it.\n\n### File operations\n\n- [ ] Right-click -> Create a new folder, then enter it.\n- [ ] Right-click in the empty folder -> Create a new file.\n- [ ] Navigate to the parent folder, create another new file, then drag it into the created folder.\n- [ ] Files can be renamed.\n- [ ] Files can be opened with non-default apps & browsing store for new apps works.\n- [ ] Normal right-click shows `Move to trash` option.\n- [ ] Shift right-click, and right-click followed by Shift, both show `Permanently delete` option.\n\n### Advanced navigation & view settings\n\n- [ ] Image and video thumbnails generate & display in local folders.\n- [ ] Gallery preview shows with Spacebar.\n- [ ] Details pane shows with Ctrl+Spacebar.\n- [ ] Zoom in/out and reset to default zoom work.\n- [ ] Ctrl+1 and Ctrl+2 switch between list and icon view.\n- [ ] Ctrl+H shows/hides hidden files.\n- [ ] Directories can be sorted at top or inline.\n- [ ] Settings -> Theme works.\n- [ ] Settings -> Type to Search affects behavior as designed.\n- [ ] Single-click to open setting takes effect.\n- [ ] Sorting options work.\n- [ ] Cutting, copying, and pasting files works.\n- [ ] F5 reloads current directory.\n- [ ] Left sidebar can be collapsed and expanded.\n\n### External filesystems\n\n- [ ] Add a network drive (e.g. SFTP) and navigate into it.\n- [ ] Plug in a USB drive; able to mount, browse, and eject.\n\n### Integrations\n\n- [ ] Desktop icons display as expected\n- [ ] Drag-and-drop into Firefox works\n"
  },
  {
    "path": "build.rs",
    "content": "use std::path::PathBuf;\nuse std::{env, fs};\nuse xdgen::{App, Context, FluentString};\n\nfn main() {\n    let id = \"com.system76.CosmicFiles\";\n    let ctx = Context::new(\"i18n\", env::var(\"CARGO_PKG_NAME\").unwrap()).unwrap();\n    let app = App::new(FluentString(\"cosmic-files\"))\n        .comment(FluentString(\"comment\"))\n        .keywords(FluentString(\"keywords\"));\n    let output = PathBuf::from(\"target/xdgen\");\n    fs::create_dir_all(&output).unwrap();\n    fs::write(\n        output.join(format!(\"{}.desktop\", id)),\n        app.expand_desktop(format!(\"res/{}.desktop\", id), &ctx)\n            .unwrap(),\n    )\n    .unwrap();\n    fs::write(\n        output.join(format!(\"{}.metainfo.xml\", id)),\n        app.expand_metainfo(format!(\"res/{}.metainfo.xml\", id), &ctx)\n            .unwrap(),\n    )\n    .unwrap();\n}\n"
  },
  {
    "path": "cosmic-files-applet/Cargo.toml",
    "content": "[package]\nname = \"cosmic-files-applet\"\nversion = \"1.0.12\"\nedition = \"2024\"\n\n[dependencies]\nlog = \"0.4\"\nzbus = \"4\" # Blocking connection in zbus 5 hangs\n\n[dependencies.cosmic-files]\npath = \"..\"\ndefault-features = false\nfeatures = [\"desktop\", \"gvfs\", \"wayland\", \"desktop-applet\"]\n"
  },
  {
    "path": "cosmic-files-applet/src/file_manager.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n// Implementation of https://www.freedesktop.org/wiki/Specifications/file-manager-interface/\n\n#![allow(dead_code, non_snake_case)]\n\nuse std::process;\n\npub struct FileManager;\n\nimpl FileManager {\n    //TODO: return error?\n    fn open(&self, uris: &[&str], _startup_id: &str) {\n        match process::Command::new(\"cosmic-files\").args(uris).spawn() {\n            Ok(mut child) => {\n                log::info!(\"spawned cosmic-files with id {:?}\", child.id());\n                match child.wait() {\n                    Ok(status) => {\n                        if status.success() {\n                            log::info!(\"cosmic-files exited with {status}\");\n                        } else {\n                            log::warn!(\"failed to run cosmic-files: exited with {status}\");\n                        }\n                    }\n                    Err(err) => {\n                        log::warn!(\"failed to run cosmic-files: {err}\");\n                    }\n                }\n            }\n            Err(err) => {\n                log::warn!(\"failed to spawn cosmic-files: {err}\");\n            }\n        }\n    }\n}\n\n//TODO: why does &[&str] not implement Deserialize?\n#[zbus::interface(name = \"org.freedesktop.FileManager1\")]\nimpl FileManager {\n    fn ShowFolders(&self, URIs: Vec<&str>, StartupId: &str) {\n        log::warn!(\"ShowFolders {:?} {:?}\", URIs, StartupId);\n        self.open(&URIs, StartupId)\n    }\n\n    fn ShowItems(&self, URIs: Vec<&str>, StartupId: &str) {\n        log::warn!(\"ShowItems {:?} {:?}\", URIs, StartupId);\n        self.open(&URIs, StartupId)\n    }\n\n    fn ShowItemProperties(&self, URIs: Vec<&str>, StartupId: &str) {\n        log::warn!(\"ShowItemProperties {:?} {:?}\", URIs, StartupId);\n        self.open(&URIs, StartupId)\n    }\n}\n"
  },
  {
    "path": "cosmic-files-applet/src/main.rs",
    "content": "mod file_manager;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    //TODO: move file manager service to its own daemon?\n    let _conn_res = zbus::blocking::connection::Builder::session()?\n        .name(\"org.freedesktop.FileManager1\")?\n        .serve_at(\"/org/freedesktop/FileManager1\", file_manager::FileManager)?\n        .build();\n\n    cosmic_files::desktop()\n}\n"
  },
  {
    "path": "debian/changelog",
    "content": "cosmic-files (1.0.12) noble; urgency=medium\n\n  * Epoch 1.0.12 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Tue, 05 May 2026 10:23:57 -0600\n\ncosmic-files (1.0.11) noble; urgency=medium\n\n  * Epoch 1.0.11 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Tue, 14 Apr 2026 11:09:44 -0600\n\ncosmic-files (1.0.9) noble; urgency=medium\n\n  * Epoch 1.0.9 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Mon, 06 Apr 2026 15:10:13 -0600\n\ncosmic-files (1.0.8) noble; urgency=medium\n\n  * Epoch 1.0.8 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Mon, 23 Feb 2026 08:13:12 -0700\n\ncosmic-files (1.0.7) noble; urgency=medium\n\n  * Epoch 1.0.7 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Tue, 17 Feb 2026 07:58:35 -0700\n\ncosmic-files (1.0.6) noble; urgency=medium\n\n  * Epoch 1.0.6 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Thu, 05 Feb 2026 15:23:07 -0700\n\ncosmic-files (1.0.5) noble; urgency=medium\n\n  * Epoch 1.0.5 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Fri, 30 Jan 2026 17:16:28 -0700\n\ncosmic-files (1.0.4) noble; urgency=medium\n\n  * Epoch 1.0.4 version update\n\n -- Jeremy Soller <jeremy@system76.com>  Wed, 21 Jan 2026 10:16:11 -0700\n\ncosmic-files (1.0.0) jammy; urgency=medium\n\n  * Stable release.\n\n -- Jeremy Soller <jeremy@system76.com>  Mon, 29 Dec 2025 15:12:39 -0700\n\ncosmic-files (0.1.0) jammy; urgency=medium\n\n  * Initial release.\n\n -- Jeremy Soller <jeremy@system76.com>  Wed, 03 Jan 2024 13:38:21 -0700\n"
  },
  {
    "path": "debian/control",
    "content": "Source: cosmic-files\nSection: admin\nPriority: optional\nMaintainer: Jeremy Soller <jeremy@system76.com>\nBuild-Depends:\n    debhelper-compat (=13),\n    git,\n    just (>= 1.13.0),\n    libclang-dev,\n    libglib2.0-dev,\n    libxkbcommon-dev,\n    pkg-config,\n    rust-all,\nStandards-Version: 4.6.2\nHomepage: https://github.com/pop-os/cosmic-files\n\nPackage: cosmic-files\nArchitecture: amd64 arm64\nDepends: ${misc:Depends}, ${shlibs:Depends}, xdg-utils\nDescription: Cosmic File Manager\n"
  },
  {
    "path": "debian/copyright",
    "content": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: cosmic-files\nUpstream-Contact: Jeremy Soller <jeremy@system76.com>\nSource: https://github.com/pop-os/cosmic-files\nFiles: *\nCopyright: System76 <info@system76.com>\nLicense: GPL-3.0\n"
  },
  {
    "path": "debian/rules",
    "content": "#!/usr/bin/make -f\n\nexport DESTDIR = debian/cosmic-files\nexport VENDOR ?= 1\n\n%:\n\tdh $@\n\noverride_dh_auto_clean:\n\tif ! ischroot && test \"${VENDOR}\" = \"1\"; then \\\n\t\tjust vendor; \\\n\tfi\n\noverride_dh_auto_build:\n\tjust build-vendored\n\noverride_dh_auto_install:\n\tjust rootdir=$(DESTDIR) install\n"
  },
  {
    "path": "debian/source/format",
    "content": "3.0 (native)\n"
  },
  {
    "path": "debian/source/options",
    "content": "tar-ignore=.github\ntar-ignore=.vscode\ntar-ignore=vendor\ntar-ignore=target"
  },
  {
    "path": "examples/copy.rs",
    "content": "use cosmic_files::operation::recursive::{Context, Method};\nuse cosmic_files::operation::{Controller, ReplaceResult};\nuse std::error::Error;\nuse std::io;\nuse std::path::PathBuf;\n\n#[compio::main]\nasync fn main() -> Result<(), Box<dyn Error>> {\n    let mut context = Context::new(Controller::default())\n        .on_progress(|op, progress| {\n            println!(\"{:?}: {:?}\", op.to, progress);\n        })\n        .on_replace(|op, conflicting_count| {\n            Box::pin(async move {\n                println!(\n                    \"replace {:?}? (y/N) [conflicting: {}]\",\n                    op.to, conflicting_count\n                );\n                let mut line = String::new();\n                match io::stdin().read_line(&mut line) {\n                    Ok(_) => {\n                        if line == \"y\" {\n                            ReplaceResult::Replace(false)\n                        } else {\n                            ReplaceResult::Skip(false)\n                        }\n                    }\n                    Err(err) => {\n                        eprintln!(\"failed to read stdin: {}\", err);\n                        ReplaceResult::Cancel\n                    }\n                }\n            })\n        });\n\n    context\n        .recursive_copy_or_move(\n            vec![(PathBuf::from(\"test/a\"), PathBuf::from(\"test/b\"))],\n            Method::Copy,\n        )\n        .await?;\n    context\n        .recursive_copy_or_move(\n            vec![(PathBuf::from(\"test/b\"), PathBuf::from(\"test/c\"))],\n            Method::Move {\n                cross_device_copy: false,\n            },\n        )\n        .await?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "examples/desktop.rs",
    "content": "// This launches the desktop mode as a regular window for easier testing.\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    cosmic_files::desktop()\n}\n"
  },
  {
    "path": "examples/dialog.rs",
    "content": "use cosmic::app::{self, Core, Settings, Task};\nuse cosmic::iced::{Subscription, window};\nuse cosmic::{Application, Element, executor, widget};\nuse cosmic_files::dialog::{\n    Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind,\n    DialogMessage, DialogResult, DialogSettings,\n};\nuse tracing_subscriber::layer::SubscriberExt;\nuse tracing_subscriber::util::SubscriberInitExt;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let log_format = tracing_subscriber::fmt::format()\n        .pretty()\n        .without_time()\n        .with_line_number(true)\n        .with_file(true)\n        .with_target(false)\n        .with_thread_names(true);\n\n    let log_layer = tracing_subscriber::fmt::Layer::default()\n        .with_writer(std::io::stderr)\n        .event_format(log_format);\n\n    tracing_subscriber::registry()\n        .with(tracing_subscriber::EnvFilter::from_env(\"RUST_LOG\"))\n        .with(log_layer)\n        .init();\n\n    let settings = Settings::default();\n    app::run::<App>(settings, ())?;\n    Ok(())\n}\n\n#[derive(Clone, Debug)]\npub enum Message {\n    DialogMessage(DialogMessage),\n    DialogOpen(DialogKind),\n    DialogOpenImages,\n    DialogResult(DialogResult),\n}\n\npub struct App {\n    core: Core,\n    dialog_opt: Option<Dialog<Message>>,\n    result_opt: Option<DialogResult>,\n}\n\nimpl Application for App {\n    type Executor = executor::Default;\n    type Flags = ();\n    type Message = Message;\n\n    const APP_ID: &'static str = \"com.system76.CosmicFilesDialogExample\";\n\n    fn core(&self) -> &Core {\n        &self.core\n    }\n\n    fn core_mut(&mut self) -> &mut Core {\n        &mut self.core\n    }\n\n    fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Message>) {\n        (\n            Self {\n                core,\n                dialog_opt: None,\n                result_opt: None,\n            },\n            Task::none(),\n        )\n    }\n\n    fn update(&mut self, message: Message) -> Task<Message> {\n        match message {\n            Message::DialogMessage(dialog_message) => {\n                if let Some(dialog) = &mut self.dialog_opt {\n                    return dialog.update(dialog_message);\n                }\n            }\n            Message::DialogOpen(dialog_kind) => {\n                if self.dialog_opt.is_none() {\n                    let (mut dialog, task) = Dialog::new(\n                        DialogSettings::new().kind(dialog_kind),\n                        Message::DialogMessage,\n                        Message::DialogResult,\n                    );\n                    let mut tasks = vec![task];\n                    dialog.set_choices(vec![\n                        DialogChoice::ComboBox {\n                            id: \"example-combobox\".into(),\n                            label: \"Combobox\".into(),\n                            options: vec![\n                                DialogChoiceOption {\n                                    id: \"foo\".into(),\n                                    label: \"foo\".into(),\n                                },\n                                DialogChoiceOption {\n                                    id: \"bar\".into(),\n                                    label: \"bar\".into(),\n                                },\n                            ],\n                            selected: Some(0),\n                        },\n                        DialogChoice::CheckBox {\n                            id: \"example-checkbox\".into(),\n                            label: \"Checkbox\".into(),\n                            value: false,\n                        },\n                    ]);\n                    tasks.push(dialog.set_filters(\n                        vec![DialogFilter {\n                            label: \"Any file\".into(),\n                            patterns: vec![DialogFilterPattern::Glob(\"*\".into())],\n                        }],\n                        Some(0),\n                    ));\n                    self.dialog_opt = Some(dialog);\n                    return Task::batch(tasks);\n                }\n            }\n            Message::DialogOpenImages => {\n                if self.dialog_opt.is_none() {\n                    let (mut dialog, task) = Dialog::new(\n                        DialogSettings::new().kind(DialogKind::OpenFile),\n                        Message::DialogMessage,\n                        Message::DialogResult,\n                    );\n                    let mut tasks = vec![task];\n                    tasks.push(dialog.set_filters(\n                        vec![\n                            DialogFilter {\n                                label: \"Images\".into(),\n                                patterns: vec![DialogFilterPattern::Mime(\"image/*\".into())],\n                            },\n                            DialogFilter {\n                                label: \"Any file\".into(),\n                                patterns: vec![DialogFilterPattern::Glob(\"*\".into())],\n                            },\n                        ],\n                        Some(0),\n                    ));\n                    self.dialog_opt = Some(dialog);\n                    return Task::batch(tasks);\n                }\n            }\n            Message::DialogResult(result) => {\n                self.dialog_opt = None;\n                self.result_opt = Some(result);\n            }\n        }\n\n        Task::none()\n    }\n\n    fn view_window(&self, window_id: window::Id) -> Element<'_, Message> {\n        match &self.dialog_opt {\n            Some(dialog) => dialog.view(window_id),\n            None => widget::text::body(\"No dialog\").into(),\n        }\n    }\n\n    fn view(&self) -> Element<'_, Message> {\n        let mut column = widget::column::with_capacity(8).spacing(8).padding(8);\n        {\n            let mut button = widget::button::standard(\"Open File\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpen(DialogKind::OpenFile));\n            }\n            column = column.push(button);\n        }\n        {\n            let mut button = widget::button::standard(\"Open Image\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpenImages);\n            }\n            column = column.push(button);\n        }\n        {\n            let mut button = widget::button::standard(\"Open Multiple Files\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpen(DialogKind::OpenMultipleFiles));\n            }\n            column = column.push(button);\n        }\n        {\n            let mut button = widget::button::standard(\"Open Folder\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpen(DialogKind::OpenFolder));\n            }\n            column = column.push(button);\n        }\n        {\n            let mut button = widget::button::standard(\"Open Multiple Folders\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpen(DialogKind::OpenMultipleFolders));\n            }\n            column = column.push(button);\n        }\n        {\n            let mut button = widget::button::standard(\"Save File\");\n            if self.dialog_opt.is_none() {\n                button = button.on_press(Message::DialogOpen(DialogKind::SaveFile {\n                    filename: \"test\".to_string(),\n                }));\n            }\n            column = column.push(button);\n        }\n        if let Some(result) = &self.result_opt {\n            match result {\n                DialogResult::Cancel => {\n                    column = column.push(widget::text::body(\"Cancel\"));\n                }\n                DialogResult::Open(paths) => {\n                    for path in paths.iter() {\n                        column = column.push(widget::text::body(format!(\"{}\", path.display())));\n                    }\n                }\n            }\n        }\n        column.into()\n    }\n\n    fn subscription(&self) -> Subscription<Message> {\n        match &self.dialog_opt {\n            Some(dialog) => dialog.subscription(),\n            None => Subscription::none(),\n        }\n    }\n}\n"
  },
  {
    "path": "examples/gio-list.rs",
    "content": "use gio::prelude::*;\nuse std::env;\n\nfn main() {\n    let uri = env::args().nth(1).expect(\"no uri provided\");\n    let file = gio::File::for_uri(&uri);\n    for entry_res in file\n        .enumerate_children(\"*\", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)\n        .unwrap()\n    {\n        let entry = entry_res.unwrap();\n        println!(\"{:?}\", entry.display_name());\n        for attribute in entry.list_attributes(None) {\n            println!(\n                \"  {:?}: {:?}\",\n                attribute,\n                entry.attribute_as_string(&attribute)\n            );\n        }\n\n        //TODO: what is the best way to resolve shortcuts?\n        let child = if let Some(target_uri) =\n            entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)\n        {\n            gio::File::for_uri(&target_uri)\n        } else {\n            file.child(entry.name())\n        };\n        println!(\"{:?}\", child.uri());\n    }\n}\n"
  },
  {
    "path": "examples/gio-mount.rs",
    "content": "use gio::prelude::*;\nuse std::env;\n\nfn main() {\n    let uri = env::args().nth(1).expect(\"no uri provided\");\n    let context = glib::MainContext::new();\n    context.block_on(async {\n        let mount_op = gio::MountOperation::new();\n        mount_op.connect_ask_password(|mount_op, message, default_user, default_domain, flags| {\n            println!(\n                \"{}, {}, {}, {:?}\",\n                message, default_user, default_domain, flags\n            );\n            mount_op.set_anonymous(true);\n            mount_op.reply(gio::MountOperationResult::Handled);\n        });\n        let file = gio::File::for_uri(&uri);\n        let res = file\n            .mount_enclosing_volume_future(gio::MountMountFlags::empty(), Some(&mount_op))\n            .await;\n        println!(\"{:?}\", res);\n    });\n}\n"
  },
  {
    "path": "examples/gvfs.rs",
    "content": "use gio::prelude::*;\n\nfn main() {\n    let monitor = gio::VolumeMonitor::get();\n    for drive in monitor.connected_drives() {\n        println!(\"Drive: {}\", drive.name());\n        for id in drive.enumerate_identifiers() {\n            println!(\"  ID: {}={:?}\", id, drive.identifier(&id));\n        }\n        for volume in drive.volumes() {\n            println!(\"  Volume: {}\", volume.name());\n            println!(\"    UUID: {:?}\", volume.uuid());\n            for id in volume.enumerate_identifiers() {\n                println!(\"    ID: {}={:?}\", id, volume.identifier(&id));\n            }\n            if let Some(mount) = volume.get_mount() {\n                println!(\"    Mount: {}\", mount.name());\n                println!(\"      UUID: {:?}\", mount.uuid());\n            }\n        }\n    }\n\n    for mount in monitor.mounts() {\n        println!(\"Mount: {}\", mount.name());\n        println!(\"  UUID: {:?}\", mount.uuid());\n    }\n\n    for volume in monitor.volumes() {\n        println!(\"Volume: {}\", volume.name());\n        println!(\"  UUID: {:?}\", volume.uuid());\n        for id in volume.enumerate_identifiers() {\n            println!(\"  ID: {}={:?}\", id, volume.identifier(&id));\n        }\n        if let Some(mount) = volume.get_mount() {\n            println!(\"  Mount: {}\", mount.name());\n            println!(\"    UUID: {:?}\", mount.uuid());\n        }\n    }\n}\n"
  },
  {
    "path": "i18n/af/cosmic_files.ftl",
    "content": "support = Ondersteuning\ncancel = Kanselleer\nsettings = Instellings\ntheme = Tema\nlight = Lig\ndark = Donker\ncosmic-files = COSMIC Leêrs\nfile = Lêer\nopen-file = Oop 'n lêer\nconnect = Konnekteer\npassword = Wagwoord\nname = Naam\nempty-folder = Leë vouer\nempty-folder-hidden = Leë vouer (met versteekte items)\nno-results = Geen resultate gevind nie\nfilesystem = Lêerstelsel\nhome = Tuis\nnetworks = Netwerke\nnotification-in-progress = Lêerbewerkings is aan die gang.\ntrash = Asblik\nrepository = Kodebank\nopen-folder = Oop 'n vouer\nmatch-desktop = Stelselstandaard\nusername = Gebruikersnaam\nappearance = Vertoon\ndelete = Skrap\nnew-tab = Nuwe oortjie\nclose-tab = Maak oortjie toe\nquit = Sluit\nedit = Redigeer\ncopy = Kopieer\npaste = Plak\nselect-all = Selekteer alles\nreplace = Vervang\nnew-window = Nuwe venster\nsave = Stoor\nundo = Ontdoen\ncut = Knip\nview = Beeld\ndefault-size = Standaardgrootte\nzoom-out = Zoem uit\nzoom-in = Zoem in\nmenu-settings = Instellings…\n"
  },
  {
    "path": "i18n/ar/cosmic_files.ftl",
    "content": "cosmic-files = ملفات COSMIC\ncomment = مدير ملفات لسطح مكتب COSMIC\nkeywords = مجلد;ملف;مدير;\nempty-folder = مجلد فارغ\nempty-folder-hidden = مجلد فارغ (يحتوي على عناصر مخفية)\nfilesystem = نظام الملفات\nhome = منزل\ntrash = المهملات\nrecents = الأحدث\n# List view\nname = الاسم\nmodified = مُعدل\nsize = الحجم\n\n# Dialogs\n\n\n## Empty Trash Dialog\n\nempty-trash = أفرغ المهملات\nempty-trash-warning = سيتم حذف العناصر الموجودة في مجلد المهملات نهائيًا\n\n## New File/Folder Dialog\n\ncreate-new-file = أنشئ ملف جديد\ncreate-new-folder = أنشئ مجلَّد جديد\nfile-name = اسم الملف\nfolder-name = اسم المجلَّد\nfile-already-exists = يوجد ملف بهذا الاسم بالفعل\nfolder-already-exists = يوجد مجلد بهذا الاسم بالفعل\nname-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية\nname-invalid = لا يمكن أن يكون الاسم «{ $filename }»\nname-no-slashes = لا يمكن أن يحتوي الاسم على شرطات مائلة (/)\n\n## Open/Save Dialog\n\ncancel = ألغِ\nopen = افتح\nopen-file = افتح ملف\nopen-folder = افتح مجلَّد\nopen-in-new-tab = افتح في لسان جديد\nopen-in-new-window = افتح في نافذة جديدة\nopen-multiple-files = افتح عدة ملفات\nopen-multiple-folders = افتح عدة مجلّدات\nsave = احفظ\nsave-file = احفظ الملف\n\n## Rename Dialog\n\nrename-file = غيِّر اسم الملف\nrename-folder = غيِّر اسم المجلّد\n\n## Replace Dialog\n\nreplace = استبدل\nreplace-title = «{ $filename }» موجود بالفعل في هذا المكان\nreplace-warning = أتريد استبداله بالملف الذي تحفظه؟ سيكتب هذا فوق محتوى الملف.\n\n# Context Pages\n\n\n## About\n\n\n## Operations\n\npending = قيد الانتظار\nfailed = فشِل\ncomplete = اكتمل\ncopy_noun = نسخ\n\n## Open with\n\nmenu-open-with = افتح ب‍استخدام...\ndefault-app = { $name } (المبدئي)\n\n## Properties\n\n\n## Settings\n\nsettings = الإعدادات\n\n### Appearance\n\nappearance = المظهر\ntheme = النسق\nmatch-desktop = طابق سطح المكتب\ndark = داكن\nlight = فاتح\n# Context menu\nadd-to-sidebar = أضِف إلى الشريط الجانبي\nnew-file = ملف جديد...\nnew-folder = مجلد جديد...\nopen-in-terminal = افتح في الطرفية\nmove-to-trash = انقل إلى المهملات\nrestore-from-trash = استعِد من المهملات\nremove-from-sidebar = أزِل من الشريط الجانبي\nsort-by-name = رتّب حسب الاسم\nsort-by-modified = رتّب حسب التعديل\nsort-by-size = رتّب حسب الحجم\n\n# Menu\n\n\n## File\n\nfile = ملف\nnew-tab = لسان جديد\nnew-window = نافذة جديدة\nrename = غيِّر الاسم...\nclose-tab = أغلق اللسان\nquit = أنهِ\n\n## Edit\n\nedit = عدِّل\ncut = قصّ\ncopy = انسخ\npaste = ألصِق\nselect-all = حدِّد الكل\n\n## View\n\nzoom-in = قرِّب\ndefault-size = الحجم المبدئي\nzoom-out = بعِّد\nview = عرض\ngrid-view = عرض الشبكة\nlist-view = عرض اللائحة\nshow-hidden-files = أظهر الملفات المخفية\nlist-directories-first = اسرد المجلدات اولاً\nmenu-settings = الإعدادات...\nmenu-about = عن مدير ملفات COSMIC...\ndismiss = أهمِل الرسالة\nno-results = لم يُعثر على نتائج\nnetworks = الشبكات\nnotification-in-progress = عمليات الملفات جارية\nundo = تراجع\ntoday = اليوم\ndesktop-view-options = خيارات عرض سطح المكتب...\nshow-on-desktop = أظهِر على سطح المكتب\ndesktop-folder-content = محتوى مجلد سطح المكتب\nmounted-drives = محركات الأقراص المثبتة\ntrash-folder-icon = أيقونة مجلد المهملات\nicon-size-and-spacing = حجم الأيقونة والتباعد\nicon-size = حجم الأيقونة\ngrid-spacing = تباعد شبكي\ntrashed-on = مهمل\ndetails = التفاصيل\npause = ألبث\nresume = استئناف\ncreate-archive = أنشئ أرشيف\nextract-password-required = كلمة السر مطلوبة\nextract-to = استخرِج إلى...\nextract-to-title = استخرِج إلى مجلّد\nmount-error = تعذر الوصول إلى القرص\ncreate = أنشئ\nopen-item-location = افتح مكان العنصر\nset-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟\nfavorite-path-error-description =\n    تعذر فتح «{ $path }»\n    قد لا يكون «{ $path }» موجودًا أو قد لا يكون لديك إذن لفتحه.\n\n    أترغب في إزالته من الشريط الجانبي؟\nopen-with-title = كيف تريد فتح «{ $name }»؟\nread-execute = قراءة وتنفيذ\nread-write = قراءة وكتابة\nread-write-execute = قراءة وكتابة وتنفيذ\nfavorite-path-error = خطأ في فتح المجلّد\nremove = أزِل\nkeep = أبقِ\nrepository = المستودع\nsupport = الدعم\nadd-network-drive = أضِف قرص شبكة\nconnect = اتصل\nconnect-anonymously = اتصل بمجهولية\nconnecting = يتصل...\ndomain = النطاق\nenter-server-address = أدخل عنوان الخادم\nnetwork-drive-description =\n    تتضمن عناوين الخادم بادئة ميفاق وعنوانًا.\n    أمثلة: ssh://192.168.0.1 ،ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    الموافيق المتاحة، البادئة\n    AppleTalk, afp://\n    ميفاق نقل الملفات, ftp:// أو ftps://\n    نظام ملفات الشبكة, nfs://\n    كتلة رسائل الخادم, smb://\n    ميفاق نقل ملفات SSH, sftp:// أو ssh://\n    WebDAV, dav:// أو davs://\nnetwork-drive-error = تعذر الوصول القرص الشبكي\npassword = كلمة السر\nremember-password = تذكر كلمة السر\ntry-again = حاول مجددًا\nusername = اسم المستخدم\ncancelled = أُلغِيَ\nedit-history = عدِّل التأريخ\nhistory = التأريخ\nno-history = لا توجد عناصر في التأريخ.\nprogress = { $percent }٪\nprogress-cancelled = { $percent }٪، أُلغِيَ\nprogress-failed = { $percent }٪، فشل\nprogress-paused = { $percent }٪، أُلبِث\ncreating = ينشئ «{ $name }» في «{ $parent }»\ncreated = أُنشئ «{ $name }» في «{ $parent }»\nemptying-trash = جارِ تفريغ { trash } ({ $progress })...\nemptied-trash = أُفرِغت { trash }\nsetting-executable-and-launching = يعيِّن «{ $name }» كقابل للتنفيذ ويُشغِّل\nset-executable-and-launched = عيِّن «{ $name }» كقابل للتنفيذ وشُغَّل\nsetting-permissions = يعيِّن الأذونات لـ«{ $name }» إلى «{ $mode }»\nset-permissions = عيِّن أذونات «{ $name }» إلى «{ $mode }»\nrenaming = تُعاد تسمية «{ $from }» إلى «{ $to }»\nrenamed = غُيِّر اسم «{ $from }» إلى «{ $to }»\nunknown-folder = مجلد مجهول\nshow-details = أظهِر التفاصيل\ntype = النوع: { $mime }\nitems = العناصر: { $items }\nitem-size = الحجم: { $size }\nitem-created = أُنشئ في: { $created }\nitem-modified = عُدّل في: { $modified }\nitem-accessed = آخر وصول: { $accessed }\ncalculating = يحسب...\nsingle-click = نقرة واحدة للفتح\ntype-to-search = اكتب للبحث\ntype-to-search-recursive = يبحث في المجلّد الحالي وجميع المجلّدات الفرعية\ntype-to-search-enter-path = يدخل المسار إلي المجلّد أو الملف\ncompress = اضغط...\ndelete-permanently = احذف نهائيًا\neject = أخرِج\nextract-here = استخرِج\nsort-by-trashed = رتّب حسب وقت الحذف\nremove-from-recents = أزِل من الحديثة\nchange-wallpaper = غيِّر خلفية الشاشة...\ndesktop-appearance = مظهر سطح المكتب...\ndisplay-settings = إعدادات العرض...\nreload-folder = أعد تحميل المجلّد\ngallery-preview = معاينة المعرض\nsort = رتّب\nsort-a-z = أ-ي\nsort-z-a = ي-أ\nsort-newest-first = الأحدث أولاً\nsort-oldest-first = الأقدم أولاً\nsort-smallest-to-largest = من الأصغر إلى الأكبر\nsort-largest-to-smallest = من الأكبر إلي الأصغر\noperations-running =\n    { $running } { $running ->\n        [one] عملية\n       *[other] عمليات\n    } قيد التشغيل ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] عملية\n       *[other] عمليات\n    } قيد التشغيل ({ $percent }%)، { $finished } انتهى...\nbrowse-store = تصفح { $store }\nother-apps = تطبيقات أخرى\nrelated-apps = تطبيقات ذات صلة\nselected-items = العناصر { $items } المحدّدة\npermanently-delete-question = احذف نهائيًا؟\ndelete = احذف\npermanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء.\nreplace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه.\noriginal-file = الملف الأصلي\nreplace-with = استبدل بـ\napply-to-all = طبِّق على الكلّ\nkeep-both = احتفظ بكليهما\nskip = تخطَّ\nset-executable-and-launch = عيِّن كقابل للتنفيذ وشغِّل\nset-and-launch = عيِّن وشغِّل\nopen-with = افتح بـ\nowner = المالك\ngroup = المجموعة\nother = أخرى\nnone = لا شيء\nexecute-only = تنفيذ فقط\nwrite-only = كتابة فقط\nwrite-execute = كتابة وتنفيذ\nread-only = قراءة فقط\ncompressing =\n    جارِ ضغط { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من «{ $from }» إلى «{ $to }» ({ $progress })...\ncompressed =\n    ضُغط { $items } { $items ->\n        [one] عنصر\n       *[other] عنصر\n    } من «{ $from }» إلى «{ $to }»\ncopied =\n    نُسِخ { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من «{ $from }» إلى «{ $to }»\ndeleting =\n    يحذف { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { trash } ({ $progress })...\ndeleted =\n    حُذف { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { trash }\ncopying =\n    ينسخ { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من «{ $from }» إلى «{ $to }» ({ $progress })...\nextracting =\n    يستخرِج { $items } { $items ->\n        [one] عنصر\n       *[other] عنصار\n    } من «{ $from }» إلى «{ $to }» ({ $progress })...\nextracted =\n    استُخرِج { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من «{ $from }» إلى «{ $to }»\nmoving =\n    ينقل { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من «{ $from }» إلى «{ $to }» ({ $progress })...\nmoved =\n    نُقل { $items } { $items ->\n        [one] item\n       *[other] items\n    } من «{ $from }» إلى «{ $to }»\npermanently-deleting =\n    يحذف { $items } بشكل نهائي { $items ->\n        [one] عنصر\n       *[other] عناصر\n    }\npermanently-deleted =\n    حُذف { $items } بشكل نهائي { $items ->\n        [one] عنصر\n       *[other] عناصر\n    }\nremoving-from-recents =\n    يُزيل { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { recents }\nremoved-from-recents =\n    أُزيل { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { recents }\nrestoring =\n    يستعيد { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { trash } ({ $progress })...\nrestored =\n    استُعيد { $items } { $items ->\n        [one] عنصر\n       *[other] عناصر\n    } من { trash }\nempty-trash-title = أفرغ المهملات؟\ntype-to-search-select = يختار أول ملف أو مجلد مطابق\npasted-image = صورة مُلصقة\npasted-text = نص مُلصق\npasted-video = فيديو مُلصق\ncopy-to-title = حدِّد وجهة النسخ\ncopy-to-button-label = انسخ\nmove-to-title = حدِّد وجهة النقل\nmove-to-button-label = انقل\ncopy-to = انسخ إلى...\nmove-to = انقل إلى...\nshow-recents = مجلد الحديثة في الشريط الجانبي\nclear-recents-history = امحُ التأريخ الحديث\ncopy-path = انسخ المسار\nmixed = مختلط\n"
  },
  {
    "path": "i18n/be/cosmic_files.ftl",
    "content": "cosmic-files = Файлы COSMIC\nempty-folder = Пустая папка\nempty-folder-hidden = Пустая папка (са схаванымі элементамі)\nno-results = Нічога не знойдзена\nfilesystem = Файлавая сістэма\nhome = Хатняя папка\nnetworks = Сеткі\nnotification-in-progress = Ідзе аперацыя з файламі.\ntrash = Сметніца\nrecents = Нядаўняе\nundo = Адрабіць\ntoday = Сёння\n# Desktop view options\ndesktop-view-options = Параметры выгляду працоўнага стала...\nshow-on-desktop = Паказваць на працоўным стале\ndesktop-folder-content = Змесціва папкі \"Працоўны стол\"\nmounted-drives = Змантаваныя дыскі\ntrash-folder-icon = Значок папкі \"Сметніца\"\nicon-size-and-spacing = Памер і інтэрвал значкоў\nicon-size = Памер значкоў\ngrid-spacing = Інтэрвал сеткі\n# List view\nname = Назва\nmodified = Зменена\ntrashed-on = Дата выдалення\nsize = Памер\n# Progress footer\ndetails = Падрабязнасці\ndismiss = Адхіліць паведамленне\noperations-running =\n    Выконваецца { $running } { $running ->\n        [one] аперацыя\n        [few] аперацыі\n       *[other] аперацый\n    } ({ $percent }%)...\noperations-running-finished =\n    Выконваецца { $running } { $running ->\n        [one] аперацыя\n        [few] аперацыі\n       *[other] аперацый\n    } ({ $percent }%), { $finished } завершана...\npause = Паўза\nresume = Працягнуць\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Стварыць архіў\n\n## Extract Dialog\n\nextract-password-required = Патрабуецца пароль\nextract-to = Выняць у...\nextract-to-title = Выняць у папку\n\n## Empty Trash Dialog\n\nempty-trash = Ачысціць сметніцу\nempty-trash-warning = Вы сапраўды хочаце назаўсёды выдаліць усе элементы з сметніцы?\n\n## Mount Error Dialog\n\nmount-error = Немагчыма атрымаць доступ да дыска\n# New File/Folder Dialog\ncreate-new-file = Стварыць новы файл\ncreate-new-folder = Стварыць новую папку\nfile-name = Назва файла\nfolder-name = Назва папкі\nfile-already-exists = Файл з такой назвай ужо існуе.\nfolder-already-exists = Папка з такой назвай ужо існуе.\nname-hidden = Назвы, якія пачынаюцца з \".\", будуць схаваны.\nname-invalid = Назва не можа быць \"{ $filename }\".\nname-no-slashes = Назва не можа ўтрымліваць касыя рысы.\n# Open/Save Dialog\ncancel = Скасаваць\ncreate = Стварыць\nopen = Адкрыць\nopen-file = Адкрыць файл\nopen-folder = Адкрыць папку\nopen-in-new-tab = Адкрыць у новай укладцы\nopen-in-new-window = Адкрыць у новым акне\nopen-item-location = Адкрыць месцазнаходжанне элемента\nopen-multiple-files = Адкрыць некалькі файлаў\nopen-multiple-folders = Адкрыць некалькі папак\nsave = Захаваць\nsave-file = Захаваць файл\n\n## Open With Dialog\n\nopen-with-title = Як вы хочаце адкрыць \"{ $name }\"?\nbrowse-store = Прагляд { $store }\nother-apps = Іншыя праграмы\nrelated-apps = Звязаныя праграмы\n\n## Permanently delete Dialog\n\nselected-items = выбрана { $items } элементаў\npermanently-delete-question = Выдаліць назаўжды\ndelete = Выдаліць\npermanently-delete-warning = Вы ўпэўненыя, што хочаце назаўжды выдаліць { $target }? Гэта дзеянне немагчыма адмяніць.\n# Rename Dialog\nrename-file = Перайменаваць файл\nrename-folder = Перайменаваць папку\n# Replace Dialog\nreplace = Замяніць\nreplace-title = { $filename } ужо існуе ў гэтым месцы.\nreplace-warning = Вы сапраўды хочаце замяніць яго на той, які вы захоўваеце? Пры замене яго змесціва будзе перапісана.\nreplace-warning-operation = Вы хочаце замяніць яго? Пры замене яго змесціва будзе перазапісана.\noriginal-file = Зыходны файл\nreplace-with = Замяніць на\napply-to-all = Прымяніць да ўсіх\nkeep-both = Захаваць абодва\nskip = Прапусціць\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Зрабіць выканальным і запусціць\nset-executable-and-launch-description = Вы хочаце зрабіць \"{ $name }\" выканальным і запусціць?\nset-and-launch = Задаць і запусціць\n\n## Metadata Dialog\n\nopen-with = Адкрыць праз\nowner = Уладальнік\ngroup = Група\nother = Іншыя\n\n### Mode 0\n\nnone = Няма\n\n### Mode 1 (unusual)\n\nexecute-only = Толькі выкананне\n\n### Mode 2 (unusual)\n\nwrite-only = Толькі запіс\n\n### Mode 3 (unusual)\n\nwrite-execute = Запіс і выкананне\n\n### Mode 4\n\nread-only = Толькі чытанне\n\n### Mode 5\n\nread-execute = Чытанне і выкананне\n\n### Mode 6\n\nread-write = Чытанне і запіс\n\n### Mode 7\n\nread-write-execute = Чытанне, запіс і выкананне\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Памылка адкрыцця каталога\nfavorite-path-error-description =\n    Немагчыма адкрыць \"{ $path }\".\n    Магчыма, ён не існуе ці ў вас няма дазволу на яго адкрыццё.\n\n    Ці хочаце вы выдаліць яго з бакавой панэлі?\nremove = Выдаліць\nkeep = Захаваць\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Дадаць сеткавы дыск\nconnect = Падключыцца\nconnect-anonymously = Падлучыць ананімна\nconnecting = Падлучэнне...\ndomain = Дамен\nenter-server-address = Увядзіце адрас серверу\nnetwork-drive-description =\n    Адрасы сервераў ўключаюць у сябе прэфікс пратаколу і адрас.\n    Прыклад: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Даступныя пратаколы,Прэфікс\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// або ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// або ssh://\n    WebDav,dav:// або davs://\nnetwork-drive-error = Немагчыма атрымаць доступ да сеткавага дыска\npassword = Пароль\nremember-password = Запомніць пароль\ntry-again = Паўтарыць спробу\nusername = Імя карыстальніка\n\n## Operations\n\ncancelled = Скасавана\nedit-history = Гісторыя рэдагавання\nhistory = Гісторыя\nno-history = У гісторыі няма запісаў.\npending = У чаканні\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, скасавана\nprogress-paused = { $percent }%, прыпынена\nfailed = Няўдала\ncomplete = Завершана\ncompressing =\n    Сцісканне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з «{ $from }» у «{ $to }» ({ $progress })...\ncompressed =\n    Сціснута { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { $from } у { $to }\ncopy_noun = Капіяваць\ncreating = Стварэнне { $name } у { $parent }\ncreated = Створана { $name } у { $parent }\ncopying =\n    Капіяванне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з «{ $from }» у «{ $to }» ({ $progress })...\ncopied =\n    Скапіявана { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { $from } у { $to }\ndeleting =\n    Выдаленне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } са { trash } ({ $progress })...\ndeleted =\n    Выдалена { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } са { trash }\nemptying-trash = Ачыстка { trash } ({ $progress })…\nemptied-trash = Ачышчана { trash }\nextracting =\n    Выманне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з «{ $from }» у «{ $to }» ({ $progress })...\nextracted =\n    Вынята { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { $from } у { $to }\nsetting-executable-and-launching = Робім \"{ $name }\" выканальным і запускаем\nset-executable-and-launched = \"{ $name }\" зроблены выканальным і запушчаны\nsetting-permissions = Усталёўваем дазволы для \"{ $name }\" на { $mode }\nset-permissions = Дазволы для \"{ $name }\" усталяваны на { $mode }\nmoving =\n    Перамяшчэнне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з «{ $from }» у «{ $to }» ({ $progress })...\nmoved =\n    Перанесена { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { $from } у { $to }\npermanently-deleting =\n    Назаўсёды выдаляем { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    }\npermanently-deleted =\n    Назаўсёды выдалена { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    }\nrenaming = Перайменаванне { $from } у { $to }\nrenamed = Перайменавана { $from } у { $to }\nrestoring =\n    Аднаўленне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з { trash } ({ $progress })...\nrestored =\n    Адноўлена { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { trash }\nunknown-folder = невядомая папка\n\n## Open with\n\nmenu-open-with = Адкрыць праз...\ndefault-app = { $name } (па змаўчанні)\n\n## Show details\n\nshow-details = Паказаць дэталі\ntype = Тып: { $mime }\nitems = Элементаў: { $items }\nitem-size = Памер: { $size }\nitem-created = Створана: { $created }\nitem-modified = Зменена: { $modified }\nitem-accessed = Апошні доступ: { $accessed }\ncalculating = Вылічэнне...\n\n## Settings\n\nsettings = Налады\nsingle-click = Адзін клік каб адкрыць\n\n### Appearance\n\nappearance = Знешні выгляд\ntheme = Тэма\nmatch-desktop = Як у сістэме\ndark = Цёмная\nlight = Светлая\n\n### Type to Search\n\ntype-to-search = Увядзіце для пошуку\ntype-to-search-recursive = Пошук у бягучай і ўкладзеных папках\ntype-to-search-enter-path = Увод шляху да каталога або файла\n# Context menu\nadd-to-sidebar = Дадаць на бакавую панэль\ncompress = Сціснуць\ndelete-permanently = Выдаліць назаўжды\neject = Выняць\nextract-here = Выняць\nnew-file = Новы файл...\nnew-folder = Новая папка...\nopen-in-terminal = Адкрыць у кансолі\nmove-to-trash = Перамясціць у сметніцу\nrestore-from-trash = Аднавіць са сметніцы\nremove-from-sidebar = Выдаліць з бакавой панэлі\nsort-by-name = Сартаваць па назве\nsort-by-modified = Сартаваць па даце змянення\nsort-by-size = Сартаваць па памеры\nsort-by-trashed = Сартаваць па часе выдалення\n\n## Desktop\n\nchange-wallpaper = Змяніць шпалеры...\ndesktop-appearance = Выгляд працоўнага стала...\ndisplay-settings = Налады дысплэя...\n\n# Menu\n\n\n## File\n\nfile = Файл\nnew-tab = Новая ўкладка\nnew-window = Новае акно\nreload-folder = Абнавіць папку\nrename = Перайменаваць...\nclose-tab = Закрыць укладку\nquit = Выйсці\n\n## Edit\n\nedit = Рэдагаваць\ncut = Выразаць\ncopy = Скапіяваць\npaste = Уставіць\nselect-all = Вылучыць усё\n\n## View\n\nzoom-in = Павялічыць\ndefault-size = Памер па змаўчанні\nzoom-out = Паменшыць\nview = Выгляд\ngrid-view = Рэжым сеткі\nlist-view = Рэжым спіса\nshow-hidden-files = Паказваць схаваныя файлы\nlist-directories-first = Размяшчаць папкі перад файламі\ngallery-preview = Папярэдні прагляд\nmenu-settings = Налады...\nmenu-about = Пра Файлы COSMIC...\n\n## Sort\n\nsort = Сартаванне\nsort-a-z = А-Я\nsort-z-a = Я-А\nsort-newest-first = Спачатку новыя\nsort-oldest-first = Спачатку старыя\nsort-smallest-to-largest = Ад меншага да найбольшага\nsort-largest-to-smallest = Ад вялікага да найменшага\nrepository = Рэпазіторый\nsupport = Падтрымка\nprogress-failed = { $percent }%, няўдала\nremoving-from-recents =\n    Выдаленне { $items } { $items ->\n        [one] элемента\n        [few] элементы\n       *[other] элементаў\n    } з { recents }\nremoved-from-recents =\n    Выдалена { $items } { $items ->\n        [one] элемент\n        [few] элементы\n       *[other] элементаў\n    } з { recents }\nremove-from-recents = Выдаліць з нядаўніх\n"
  },
  {
    "path": "i18n/bg/cosmic_files.ftl",
    "content": "cosmic-files = Файлове на COSMIC\nempty-folder = Празна папка\nempty-folder-hidden = Празна папка (съдържа скрити елементи)\nno-results = Няма намерени резултати\nfilesystem = Файлова система\nhome = Домашна папка\nnetworks = Мрежи\nnotification-in-progress = Файлови операции са в процес на изпълнение.\ntrash = Кошче\nrecents = Скоро ползвани\nundo = Отменяне\ntoday = Днес\n# Desktop view options\ndesktop-view-options = Опции за изглед на работния плот...\nshow-on-desktop = Покажи на работния плот\ndesktop-folder-content = Съдържание на папката на работния плот\nmounted-drives = Монтирани устройства\ntrash-folder-icon = Иконка на кошчето\nicon-size-and-spacing = Размер и разстояние между иконките\nicon-size = Размер\ngrid-spacing = Разстояние\n# List view\nname = Име\nmodified = Променян\ntrashed-on = Изтрит\nsize = Размер\n# Progress footer\ndetails = Подробности\ndismiss = Отмяна на съобщението\noperations-running =\n    { $running } { $running ->\n        [one] операция се изпълнява\n       *[other] операции се изпълняват\n    } ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] операция се изпълнява\n       *[other] операции се изпълняват\n    } ({ $percent }%), { $finished } завършиха...\npause = Пауза\nresume = Продължаване\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Създаване на архив\n\n## Extract Dialog\n\nextract-password-required = Необходима е парола\nextract-to = Разархивиране в...\nextract-to-title = Разархивиране в папка\n\n## Empty Trash Dialog\n\nempty-trash = Изпразване на кошчето\nempty-trash-warning = Сигурни ли сте, че искате да изтриете завинаги всички елементи в кошчето?\n\n## Mount Error Dialog\n\nmount-error = Устройството не може да бъде достъпно\n\n## New File/Folder Dialog\n\ncreate-new-file = Създаване на нов файл\ncreate-new-folder = Създаване на нова папка\nfile-name = Име на файла\nfolder-name = Име на папката\nfile-already-exists = Вече съществува файл с това име.\nfolder-already-exists = Вече съществува папка с това име.\nname-hidden = Имената, започващи с „.“, ще бъдат скрити.\nname-invalid = Името не може да бъде „{ $filename }“.\nname-no-slashes = Името не може да съдържа наклонени черти.\n\n## Open/Save Dialog\n\ncancel = Отказване\ncreate = Създаване\nopen = Отваряне\nopen-file = Отваряне на файл\nopen-folder = Отваряне на папка\nopen-in-new-tab = Отваряне в нов раздел\nopen-in-new-window = Отваряне в нов прозорец\nopen-item-location = Отваряне на местоположението на обекта\nopen-multiple-files = Отваряне на няколко файла\nopen-multiple-folders = Отваряне на няколко папки\nsave = Запазване\nsave-file = Запазване на файла\n\n## Open With Dialog\n\nopen-with-title = Как искате да отворите „{ $name }“?\nbrowse-store = Разгледайте { $store }\nother-apps = Други програми\nrelated-apps = Свързани програми\n\n## Permanently delete Dialog\n\nselected-items = избраните { $items } елемента\npermanently-delete-question = Изтриване завинаги\ndelete = Изтриване\npermanently-delete-warning = Сигурни ли сте, че искате да изтриете завинаги { $target }? Това действие не може да бъде върнато.\n\n## Rename Dialog\n\nrename-file = Преименуване на файла\nrename-folder = Преименуване на папката\n\n## Replace Dialog\n\nreplace = Заменяне\nreplace-title = „{ $filename }“ вече съществува на това местоположение.\nreplace-warning = Искате ли да го замените с този, който запазвате? Ако го замените, ще презапишете съдържанието му.\nreplace-warning-operation = Искате ли да го замените? Ако го замените, ще презапишете съдържанието му.\noriginal-file = Съществуващ файл\nreplace-with = Замяна с\napply-to-all = Прилагане за всички\nkeep-both = Запазване на и двата\nskip = Пропускане\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Задаване като изпълним и стартиране\nset-executable-and-launch-description = Искате ли да зададете „{ $name }“ като изпълним и да го стартирате?\nset-and-launch = Задаване и стартиране\n\n## Metadata Dialog\n\nopen-with = Отваряне с\nowner = Собственик\ngroup = Група\nother = Друго\n\n### Mode 0\n\nnone = Без\n\n### Mode 1 (unusual)\n\nexecute-only = Само за изпълняване\n\n### Mode 2 (unusual)\n\nwrite-only = Само за записване\n\n### Mode 3 (unusual)\n\nwrite-execute = Записване и изпълняване\n\n### Mode 4\n\nread-only = Само за четене\n\n### Mode 5\n\nread-execute = Четене и изпълняване\n\n### Mode 6\n\nread-write = Четене и записване\n\n### Mode 7\n\nread-write-execute = Четене, записване и изпълняване\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Грешка при отваряне на папката\nfavorite-path-error-description =\n    Местоположението „{ $path }“ не може да бъде отворено.\n    Възможно е да не съществува или да нямате права да го отворите.\n\n    Искате ли да го премахнете от страничната лента?\nremove = Премахване\nkeep = Запазване\n\n# Context Pages\n\n\n## About\n\nrepository = Хранилище\nsupport = Поддръжка\n\n## Add Network Drive\n\nadd-network-drive = Добавяне на мрежово устройство\nconnect = Свързване\nconnect-anonymously = Свързване анонимно\nconnecting = Свързване...\ndomain = Домейн\nenter-server-address = Въведете адрес на сървър\nnetwork-drive-description =\n    Адресите на сървърите включват представка на протокола и адрес.\n    Примери: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Налични протоколи,Представка\n    AppleTalk,afp://\n    Протокол за пренос на файлове,ftp:// или ftps://\n    Мрежова файлова система,nfs://\n    Server Message Block,smb://\n    Пренос на файлове по SSH,sftp:// или ssh://\n    WebDav,dav:// или davs://\nnetwork-drive-error = Мрежовото устройство не може да бъде достъпно\npassword = Парола\nremember-password = Запомняне на паролата\ntry-again = Опитайте отново\nusername = Потребителско име\n\n## Operations\n\ncancelled = Отменена\nedit-history = Редактиране на историята\nhistory = История\nno-history = Няма елементи в историята.\npending = Чакащи\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, отменена\nprogress-failed = { $percent }%, неуспешно\nprogress-paused = { $percent }%, на пауза\nfailed = Неуспешна\ncomplete = Завършена\ncompressing =\n    Компресиране на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“ ({ $progress })...\ncompressed =\n    Компресирано е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“\ncopy_noun = Копиране\ncreating = Създаване на „{ $name }“ в „{ $parent }“\ncreated = „{ $name }“ е създадено в „{ $parent }“\ncopying =\n    Копиране на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“ ({ $progress })...\ncopied =\n    Копирано е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“\ndeleting =\n    Изтриване на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { trash } ({ $progress })...\ndeleted =\n    Изтрито е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { trash }\nemptying-trash = Изпразване на { trash } ({ $progress })...\nemptied-trash = { trash } е изпразнено\nextracting =\n    Извличане на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“ ({ $progress })...\nextracted =\n    Извлечено е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“\nsetting-executable-and-launching = Задаване на „{ $name }“ като изпълним и стартиране\nset-executable-and-launched = „{ $name }“ е зададен като изпълним и е стартиран\nsetting-permissions = Задаване на правата за { $name }\" на { $mode }\nset-permissions = Правата за \"{ $name }\" бяха зададени на { $mode }\nmoving =\n    Преместване на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“ ({ $progress })...\nmoved =\n    Преместено е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от „{ $from }“ в „{ $to }“\npermanently-deleting =\n    Изтриване завинаги на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    }\npermanently-deleted =\n    Изтрито е завинаги { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    }\nremoving-from-recents =\n    Премахване на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { recents }\nremoved-from-recents =\n    Премахнато е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { recents }\nrenaming = Преименуване на „{ $from }“ на „{ $to }“\nrenamed = „{ $from }“ е преименувано на „{ $to }“\nrestoring =\n    Възстановяване на { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { trash } ({ $progress })...\nrestored =\n    Възстановено е { $items } { $items ->\n        [one] елемент\n       *[other] елемента\n    } от { trash }\nunknown-folder = неизвестна папка\n\n## Open with\n\nmenu-open-with = Отваряне с...\ndefault-app = { $name } (стандартно)\n\n## Show details\n\nshow-details = Показване на подробностите\ntype = Вид: { $mime }\nitems = Елементи: { $items }\nitem-size = Размер: { $size }\nitem-created = Създаден: { $created }\nitem-modified = Променян: { $modified }\nitem-accessed = Достъпен: { $accessed }\ncalculating = Изчисляване...\n\n## Settings\n\nsettings = Настройки\nsingle-click = Отваряне с едно натискане\n\n### Appearance\n\nappearance = Външен вид\ntheme = Тема\nmatch-desktop = Системна тема\ndark = Тъмна тема\nlight = Светла тема\n\n### Type to Search\n\ntype-to-search = Въведете текст за търсене\ntype-to-search-recursive = Търсене в текущата папка и всички подпапки\ntype-to-search-enter-path = Въвежда пътя до папката или файла\n# Context menu\nadd-to-sidebar = Добавяне към страничната лента\ncompress = Компресиране\ndelete-permanently = Изтриване завинаги\neject = Изваждане\nextract-here = Разархивиране\nnew-file = Нов файл...\nnew-folder = Нова папка...\nopen-in-terminal = Отваряне в терминала\nmove-to-trash = Преместване в кошчето\nrestore-from-trash = Възстановяване от кошчето\nremove-from-sidebar = Премахване от стр. лента\nsort-by-name = Подреждане по име\nsort-by-modified = Подреждане по дата на променяне\nsort-by-size = Подреждане по размер\nsort-by-trashed = Подреждане по дата на изтриване\nremove-from-recents = Премахване от скорошни\n\n## Desktop\n\nchange-wallpaper = Променяне на фона...\ndesktop-appearance = Външен вид на работния плот...\ndisplay-settings = Настройки на екрана...\n\n# Menu\n\n\n## File\n\nfile = Файл\nnew-tab = Нов подпрозорец\nnew-window = Нов прозорец\nreload-folder = Презареждане на папката\nrename = Преименуване...\nclose-tab = Затваряне на подпрозореца\nquit = Спиране на програмата\n\n## Edit\n\nedit = Редактиране\ncut = Отрязване\ncopy = Копиране\npaste = Поставяне\nselect-all = Избор на всички\n\n## View\n\nzoom-in = Увеличаване\ndefault-size = Стандартен размер\nzoom-out = Намаляване\nview = Изглед\ngrid-view = Изглед като решетка\nlist-view = Изглед като списък\nshow-hidden-files = Показване на скритите файлове\nlist-directories-first = Изброяване първо на папките\ngallery-preview = Изглед като галерия\nmenu-settings = Настройки...\nmenu-about = Относно „Файлове на COSMIC“...\n\n## Sort\n\nsort = Подреждане\nsort-a-z = А→Я\nsort-z-a = Я→А\nsort-newest-first = Най-новите първи\nsort-oldest-first = Най-старите първи\nsort-smallest-to-largest = Най-малките до най-големите\nsort-largest-to-smallest = Най-големите до най-малките\n"
  },
  {
    "path": "i18n/bn/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/ca/cosmic_files.ftl",
    "content": "cosmic-files = Fitxers del COSMIC\nempty-folder = Carpeta buida\nempty-folder-hidden = Carpeta buida (té elements ocults)\nno-results = No s'ha trobat cap resultat\nfilesystem = Sistema de fitxers\nhome = Inici\nnetworks = Xarxes\nnotification-in-progress = Hi ha operacions amb fitxers en curs.\ntrash = Paperera\nrecents = Recents\nundo = Desfés\ntoday = Avui\n# Desktop view options\ndesktop-view-options = Opcions de visualització de l'escriptori\nshow-on-desktop = Mostra a l'escriptori\ndesktop-folder-content = Contingut de la carpeta de l'escriptori\nmounted-drives = Unitats muntades\ntrash-folder-icon = Icona de la paperera\nicon-size-and-spacing = Mida i espaiat de les icones\nicon-size = Mida de les icones\ngrid-spacing = Espaiat de la quadrícula\n# List view\nname = Nom\nmodified = Modificat\ntrashed-on = Mogut a la paperera\nsize = Mida\n# Progress footer\ndetails = Detalls\ndismiss = Descarta el missatge\noperations-running =\n    { $running } { $running ->\n        [one] operacion\n       *[other] operacions\n    } en curs ({ $percent }%)...\noperations-running-finished =\n    { $running }{ $running ->\n        [one] operacion\n       *[other] operacions\n    } en curs ({ $percent }%), { $finished } acabat...\npause = Pausa\nresume = Reprèn\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Crea un arxiu\n\n## Extract Dialog\n\nextract-password-required = Cal una contrasenya\n\n## Empty Trash Dialog\n\nempty-trash = Buida la paperera\nempty-trash-warning = Voleu suprimir permanentment tots els fitxers de la paperera?\n\n## Mount Error Dialog\n\nmount-error = No es pot accedir a la unitat\n\n## New File/Folder Dialog\n\ncreate-new-file = Crea un nou fitxer\ncreate-new-folder = Crea una nova carpeta\nfile-name = Nom del fixer\nfolder-name = Nom de la carpeta\nfile-already-exists = Ja existeix un fitxer amb aquest nom.\nfolder-already-exists = Ja existeix una carpeta amb aquest nom.\nname-hidden = Els noms que comencin amb \".\" seran ocults.\nname-invalid = El nom no pot ser \"{ $filename }\".\nname-no-slashes = El nom no pot contenir barres.\n\n## Open/Save Dialog\n\ncancel = Cancel·la\ncreate = Crea\nopen = Obre\nopen-file = Obre el fixer\nopen-folder = Obre la carpeta\nopen-in-new-tab = Obre en una pestanya nova\nopen-in-new-window = Obre en una finestra nova\nopen-item-location = Obre la ubicació del fitxer\nopen-multiple-files = Obre múltiples fitxers\nopen-multiple-folders = Obre múltiples carpetes\nsave = Desa\nsave-file = Desa el fitxer\n\n## Open With Dialog\n\nopen-with-title = Com voleu obrir \"{ $name }\"?\nbrowse-store = Navega { $store }\n\n## Rename Dialog\n\nrename-file = Canvia el nom del fitxer\nrename-folder = Canvia el nom de la carpeta\n\n## Replace Dialog\n\nreplace = Reemplaça\nreplace-title = Ja existeix \"{ $filename }\" en aquesta ubicació.\nreplace-warning = Voleu reemplaçar-lo pel fitxer que esteu desant? El seu contingut serà sobreescrit.\nreplace-warning-operation = Voleu reemplaçar-lo? El seu contingut serà sobreescrit.\noriginal-file = Fitxer original\nreplace-with = Reemplaça amb\napply-to-all = Aplica-ho a tot\nkeep-both = Mantén els dos\nskip = Omet\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Defineix com a executable i executa\nset-executable-and-launch-description = Voleu definir \"{ $name }\" com a executable i executar-lo?\nset-and-launch = Defineix i executa\n\n## Metadata Dialog\n\nopen-with = Obre amb\nowner = Propietari\ngroup = Grup\nother = Altre\n\n### Mode 0\n\nnone = Cap\n\n### Mode 1 (unusual)\n\nexecute-only = Només executar\n\n### Mode 2 (unusual)\n\nwrite-only = Només escriure\n\n### Mode 3 (unusual)\n\nwrite-execute = Escriure i executar\n\n### Mode 4\n\nread-only = Només llegir\n\n### Mode 5\n\nread-execute = Llegir i executar\n\n### Mode 6\n\nread-write = Llegir i escriure\n\n### Mode 7\n\nread-write-execute = Llegir, escriure i executar\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Afegeix una unitat de la xarxa\nconnect = Connecta\nconnect-anonymously = Connecta anònimament\nconnecting = Connectant...\ndomain = Domini\nenter-server-address = Introduïu l'adreça del servidor\nnetwork-drive-description =\n    Les adreces de servidor estan formades per un prefix del protocol i una adreça.\n    Examples: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocols disponibles,Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDav,dav:// or davs://\nnetwork-drive-error = No s'ha pogut accedir a la unitat de la xarxa\npassword = Contrasenya\nremember-password = Recorda la contrasenya\ntry-again = Torna-ho a provar\nusername = Nom d'usuari\n\n## Operations\n\ncancelled = Cancel·lat\nedit-history = Edita l'historial\nhistory = Historial\nno-history = Historial buit.\npending = Pendent\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, cancel·lat\nprogress-paused = { $percent }%, en pausa\nfailed = Ha fallat\ncomplete = Complet\ncompressing =\n    { $items ->\n        [one] S'està comprimint { $items } element\n       *[other] S'estan comprimint { $items } elements\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items ->\n        [one] S'ha comprimit { $items } element\n       *[other] S'han comprimit { $items } elements\n    } de \"{ $from }\" a \"{ $to }\"\ncopy_noun = Copia\ncreating = S'està creant \"{ $name }\" a \"{ $parent }\"\ncreated = S'ha creat \"{ $name }\" a \"{ $parent }\"\ncopying =\n    { $items ->\n        [one] S'està copiant { $items } element\n       *[other] S'estan copiant { $items } elements\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncopied =\n    { $items ->\n        [one] S'ha copiat { $items } element\n       *[other] S'han copiat { $items } elements\n    } de \"{ $from }\" a \"{ $to }\"\nemptying-trash = S'està buidant { trash } ({ $progress })...\nemptied-trash = S'ha buidat { trash }\nextracting =\n    { $items ->\n        [one] S'està extraient { $items } element\n       *[other] S'estan extraient { $items } elements\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nextracted =\n    { $items ->\n        [one] S'ha extret { $items } element\n       *[other] S'han extret { $items } elements\n    } de \"{ $from }\" a \"{ $to }\"\nsetting-executable-and-launching = S'està definint \"{ $name }\" com a executable i executant\nset-executable-and-launched = S'ha definit \"{ $name }\" com a executable i executat\nmoving =\n    { $items ->\n        [one] S'està movent { $items } element\n       *[other] S'estan movent { $items } elements\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nmoved =\n    { $items ->\n        [one] S'ha mogut { $items } element\n       *[other] S'han mogut { $items } elements\n    } de \"{ $from }\" a \"{ $to }\"\nrenaming = S'està canviant el nom de \"{ $from }\" a \"{ $to }\"\nrenamed = S'ha canviat el nom de \"{ $from }\" a \"{ $to }\"\nrestoring =\n    { $items ->\n        [one] S'està restaurant { $items } element\n       *[other] S'estan restaurant { $items } elements\n    } de { trash } ({ $progress })...\nrestored =\n    { $items ->\n        [one] S'ha restaurat { $items } element\n       *[other] S'han restaurat { $items } elements\n    } de { trash }\nunknown-folder = carpeta desconeguda\n\n## Open with\n\nmenu-open-with = Obre amb...\ndefault-app = { $name } (per defecte)\n\n## Show details\n\nshow-details = Mostra els detalls\ntype = Tipus: { $mime }\nitems = Elements: { $items }\nitem-size = Mida: { $size }\nitem-created = Creat: { $created }\nitem-modified = Modificat: { $modified }\nitem-accessed = Accedit: { $accessed }\ncalculating = S'està calculant...\n\n## Settings\n\nsettings = Configuració\n\n### Appearance\n\nappearance = Aparença\ntheme = Tema\nmatch-desktop = Coincideix amb l'escriptori\ndark = Fosc\nlight = Clar\n# Context menu\nadd-to-sidebar = Afegeix a la barra lateral\ncompress = Comprimeix\nextract-here = Extreu\nnew-file = Nou fitxer...\nnew-folder = Nova carpeta...\nopen-in-terminal = Obre al terminal\nmove-to-trash = Mou a la paperera\nrestore-from-trash = Restaura de la paperera\nremove-from-sidebar = Elimina de la barra lateral\nsort-by-name = Ordena per nom\nsort-by-modified = Ordena per data de modificació\nsort-by-size = Ordena per mida\nsort-by-trashed = Ordena per data de supressió\n\n## Desktop\n\nchange-wallpaper = Canvia el fons de pantalla...\ndesktop-appearance = Aparença de l'escriptori...\ndisplay-settings = Configuració de visualització...\n\n# Menu\n\n\n## File\n\nfile = Fitxer\nnew-tab = Pestanya nova\nnew-window = Finestra nova\nrename = Canvia el nom...\nclose-tab = Tanca la pestanya\nquit = Surt\n\n## Edit\n\nedit = Edita\ncut = Retalla\ncopy = Copia\npaste = Enganxa\nselect-all = Selecciona-ho tot\n\n## View\n\nzoom-in = Amplia\ndefault-size = Mida predeterminada\nzoom-out = Redueix\nview = Visualització\ngrid-view = Vista de graella\nlist-view = Vista de llista\nshow-hidden-files = Mostra els fitxers ocults\nlist-directories-first = Mostra els directoris primer\ngallery-preview = Vista prèvia en galeria\nmenu-settings = Configuració...\nmenu-about = Quant a Fitxers del COSMIC...\n\n## Sort\n\nsort = Ordena\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Primer més recents\nsort-oldest-first = Primer més antics\nsort-smallest-to-largest = De petit a gran\nsort-largest-to-smallest = De gran a petit\n"
  },
  {
    "path": "i18n/cs/cosmic_files.ftl",
    "content": "cosmic-files = Soubory COSMIC\ncomment = Správce souborů pro prostředí COSMIC\nkeywords = Složka;Složky;Správce;Manažer;Prohlížeč;\nempty-folder = Složka je prázdná\nempty-folder-hidden = Složka je prázdná (obsahuje skryté položky)\nfilesystem = Souborový systém\nhome = Domů\nrecents = Nedávné\ntrash = Koš\n# List view\nname = Název\nmodified = Datum změny\nsize = Velikost\n\n# Dialogs\n\n\n## Empty Trash Dialog\n\nempty-trash = Vysypat koš\nempty-trash-warning = Položky v koši budou trvale smazány\n\n## New File/Folder Dialog\n\ncreate-new-file = Vytvořit nový soubor\ncreate-new-folder = Vytvořit novou složku\nfile-name = Název souboru\nfolder-name = Název složky\nfile-already-exists = Již existuje soubor s daným názvem\nfolder-already-exists = Již existuje složka s daným názvem\nname-hidden = Položky s názvem začínajícím tečkou budou skryty\nname-invalid = Název nemůže být „{ $filename }“\nname-no-slashes = Název nesmí obsahovat lomítka\n\n## Open/Save Dialog\n\ncancel = Zrušit\nopen = Otevřít\nopen-file = Otevřít soubor\nopen-folder = Otevřít složku\nopen-in-new-tab = Otevřít na nové kartě\nopen-in-new-window = Ovevřít v novém okně\nopen-multiple-files = Otevřít více souborů\nopen-multiple-folders = Otevřít více složek\nsave = Uložit\nsave-file = Uložit soubor\n\n## Rename Dialog\n\nrename-file = Přejmenovat soubor\nrename-folder = Přejmenovat složku\n\n## Replace Dialog\n\nreplace = Nahradit\nreplace-title = Soubor „{ $filename }“ již na daném místě existuje\nreplace-warning = Chcete nahradit soubor tím, který ukládáte? Nahrazení přepíše veškerý jeho obsah.\n\n# Context Pages\n\n\n## About\n\n\n## Operations\n\npending = Nevyřízené\nfailed = Neúspěšné\ncomplete = Dokončené\ncopy_noun = Kopie\n\n## Open with\n\nmenu-open-with = Otevřít pomocí...\ndefault-app = { $name } (výchozí)\n\n## Properties\n\n\n## Settings\n\nsettings = Nastavení\n\n### Appearance\n\nappearance = Vzhled\ntheme = Motiv\nmatch-desktop = Podle systému\ndark = Tmavý\nlight = Světlý\n# Context menu\nadd-to-sidebar = Přidat do postranního panelu\nnew-file = Nový soubor...\nnew-folder = Nová složka...\nopen-in-terminal = Otevřít v terminálu\nmove-to-trash = Přesunout do koše\nrestore-from-trash = Obnovit z koše\nremove-from-sidebar = Odstranit z postranního panelu\nsort-by-name = Seřadit podle názvu\nsort-by-modified = Seřadit podle data změny\nsort-by-size = Seřadit podle velikosti\n\n# Menu\n\n\n## File\n\nfile = Soubor\nnew-tab = Nová karta\nnew-window = Nové okno\nrename = Přejmenovat...\nclose-tab = Zavřít kartu\nquit = Ukončit\n\n## Edit\n\nedit = Úpravy\ncut = Vyjmout\ncopy = Kopírovat\npaste = Vložit\nselect-all = Vybrat vše\n\n## View\n\nzoom-in = Přiblížit\ndefault-size = Výchozí velikost\nzoom-out = Oddálit\nview = Zobrazení\ngrid-view = Zobrazit jako mřížku\nlist-view = Zobrazit jako seznam\nshow-hidden-files = Zobrazit skryté soubory\nlist-directories-first = Řadit nejprve složky\nmenu-settings = Nastavení...\nmenu-about = O aplikaci Soubory COSMIC...\nno-results = Nenalezeny žádné výsledky\nrepository = Repozitář\nsupport = Podpora\nnetworks = Sítě\nnotification-in-progress = Probíhají operace se soubory\nundo = Vrátit\nconnect = Připojit\ntoday = Dnes\ndesktop-view-options = Možnosti zobrazení plochy...\nshow-on-desktop = Zobrazit na ploše\ndesktop-folder-content = Obsah složky na ploše\nmounted-drives = Připojené disky\ntrash-folder-icon = Ikona koše\nicon-size = Velikost ikony\npassword = Heslo\nremove = Odstranit\nusername = Uživatelské jméno\ndetails = Podrobnosti\npause = Pozastavit\nresume = Pokračovat\ncreate-archive = Vytvořit archiv\nextract-password-required = Vyžadováno heslo\nextract-to = Rozbalit do...\nextract-to-title = Rozbalit do složky\nmount-error = Nelze přistoupit k disku\ncreate = Vytvořit\nopen-item-location = Otevřít umístění položky\nopen-with-title = Jak chcete otevřít „{ $name }“?\nbrowse-store = Procházet { $store }\nother-apps = Ostatní aplikace\nrelated-apps = Související aplikace\npermanently-delete-question = Trvale smazat?\ndelete = Smazat\npermanently-delete-warning = Dojde k trvalému smazání { $target }. Tuto akci nelze vrátit.\nreplace-warning-operation = Chcete soubor nahradit? Nahrazení přepíše veškerý jeho obsah.\noriginal-file = Původní soubor\nreplace-with = Nahradit za\nkeep-both = Ponechat oba\nskip = Přeskočit\nset-executable-and-launch = Povolit spouštění a spustit\nset-executable-and-launch-description = Chcete povolit spouštění souboru „{ $name }“ a následně ho spustit?\nset-and-launch = Povolit a spustit\nopen-with = Otevřít pomocí\nother = Ostatní\nnone = Žádné\nicon-size-and-spacing = Velikost a rozestupy ikon\ngrid-spacing = Rozestupy mřížky\ndeleting =\n    Mazání { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z koše ({ $progress })...\nsort-by-trashed = Seřadit podle času smazání\ndeleted =\n    { $items ->\n        [one] Smazána\n        [few] Smazány\n       *[other] Smazáno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z koše\nemptying-trash = Vysypávání koše ({ $progress })...\nemptied-trash = Koš vysypán\nrestoring =\n    Obnovování { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z koše ({ $progress })...\nrestored =\n    { $items ->\n        [one] Obnovena\n        [few] Obnoveny\n       *[other] Obnoveno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z koše\npermanently-deleted =\n    Trvale { $items ->\n        [one] smazána\n        [few] smazány\n       *[other] smazáno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    }\ndelete-permanently = Smazat trvale\ntrashed-on = Smazáno\ndismiss = Zavřít zprávu\noperations-running =\n    Běží { $running } { $running ->\n        [one] operace\n        [few] operace\n       *[other] operací\n    } ({ $percent }%)...\noperations-running-finished =\n    Běží { $running } { $running ->\n        [one] operace\n        [few] operace\n       *[other] operací\n    } ({ $percent }%), { $finished } { $finished ->\n        [one] dokončena...\n        [few] dokončeny...\n       *[other] dokončeno...\n    }\napply-to-all = Použít na vše\nowner = Vlastník\ngroup = Skupina\nexecute-only = Pouze spouštění\nwrite-only = Pouze zápis\nwrite-execute = Zápis a spouštění\nread-only = Pouze čtení\nadd-network-drive = Přidat síťový disk\nconnect-anonymously = Připojit se anonymně\nconnecting = Připojování...\ndomain = Doména\nenter-server-address = Zadejte adresu serveru\nnetwork-drive-description =\n    Adresy serveru obsahují prefix protokolu a adresu.\n    Příklady: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Dostupné protokoly,Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// nebo ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// nebo ssh://\n    WebDAV,dav:// nebo davs://\nnetwork-drive-error = Nelze přistoupit k síťovému disku\nremember-password = Zapamatovat heslo\ntry-again = Zkusit znovu\ncancelled = Zrušené\nedit-history = Historie úprav\nhistory = Historie\nno-history = Žádné položky v historii.\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, zrušeno\nprogress-failed = { $percent }%, selhalo\nprogress-paused = { $percent }%, pozastaveno\nkeep = Ponechat\ncompressing =\n    Balení { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“ ({ $progress })...\ncompressed =\n    { $items ->\n        [one] Zabalena\n        [few] Zabaleny\n       *[other] Zabaleno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“\ncreating = Vytváření položky „{ $name }“ v „{ $parent }“\ncreated = Vytvořena položka „{ $name }“ v „{ $parent }“\ncopying =\n    Kopírování { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“ ({ $progress })...\ncopied =\n    { $items ->\n        [one] Zkopírována\n        [few] Zkopírovány\n       *[other] Zkopírováno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“\nextracting =\n    Rozbalování { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“ ({ $progress })...\nfavorite-path-error-description =\n    Nelze otevřít „{ $path }“\n    „{ $path }“ buď neexistuje nebo nemáte dostatečná práva pro otevření\n\n    Chcete položku odstranit z postranního panelu?\nselected-items = { $items } vybraných položek\nread-execute = Čtení a spouštění\nread-write-execute = Čtení, zápis a spouštění\nread-write = Čtení a zápis\nfavorite-path-error = Chyba otevírání složky\nextracted =\n    { $items ->\n        [one] Rozbalena\n        [few] Rozbaleny\n       *[other] Rozbaleno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“\nsetting-executable-and-launching = Nastavování souboru „{ $name }“ jako spustitelného a spouštění\nset-executable-and-launched = Soubor „{ $name }“ nastaven jako spustitelný a spuštěn\nsetting-permissions = Nastavování práv položky „{ $name }“ na { $mode }\nset-permissions = Práva položky „{ $name }“ nastavena na { $mode }\nmoving =\n    Přesouvání { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“ ({ $progress })...\nmoved =\n    { $items ->\n        [one] Přesunuta\n        [few] Přesunuty\n       *[other] Přesunuto\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z „{ $from }“ do „{ $to }“\npermanently-deleting =\n    Trvalé mazání { $items } { $items ->\n        [one] položky\n       *[other] položek\n    }\nremoving-from-recents =\n    Odstraňování { $items } { $items ->\n        [one] položky\n       *[other] položek\n    } z { recents }\nremoved-from-recents =\n    { $items ->\n        [one] Odstraněna\n        [few] Odstraněny\n       *[other] Odstraněno\n    } { $items } { $items ->\n        [one] položka\n        [few] položky\n       *[other] položek\n    } z { recents }\nremove-from-recents = Odstranit z nedávných\nrenaming = Přejmenování „{ $from }“ na „{ $to }“\nrenamed = Přejmenováno „{ $from }“ na „{ $to }“\nunknown-folder = neznámá složka\nshow-details = Zobrazit podrobnosti\ntype = Typ: { $mime }\nitems = Položky: { $items }\nitem-size = Velikost: { $size }\nitem-created = Vytvořeno: { $created }\nitem-modified = Změněno: { $modified }\nitem-accessed = Poslední přístup: { $accessed }\ncalculating = Vypočítávání...\nsingle-click = Otevřít jedním kliknutím\ntype-to-search = Vyhledávání psaním\ntype-to-search-recursive = Prohledává aktuální složku a její podsložky\ntype-to-search-enter-path = Zadává cestu ke složce nebo souboru\ncompress = Zabalit...\neject = Vysunout\nextract-here = Rozbalit\nchange-wallpaper = Změnit tapetu...\ndesktop-appearance = Vzhled plochy...\ndisplay-settings = Nastavení obrazovky...\nreload-folder = Znovu načíst složku\nsort-z-a = Z-A\nsort-newest-first = Nejnovější první\nsort-oldest-first = Nejstarší první\nsort-smallest-to-largest = Od nejmenšího po největší\nsort-largest-to-smallest = Od největšího po nejmenší\ngallery-preview = Náhled galerie\nsort = Řazení\nsort-a-z = A-Z\nempty-trash-title = Vysypat koš?\ntype-to-search-select = Vybere první shodující se soubor nebo složku\npasted-image = Vložený obrázek\npasted-text = Vložený text\npasted-video = Vložené video\ncopy-to-button-label = Kopírovat\nmove-to-button-label = Přesunout\ncopy-to = Kopírovat do...\nmove-to = Přesunout do...\ncopy-to-title = Vyberte cíl kopírování\nmove-to-title = Vyberte cíl přesunutí\nshow-recents = Složka „Nedávné“ v postranním panelu\ncopy-path = Kopírovat cestu\nclear-recents-history = Vymazat historii „Nedávné“\nmixed = Různé\n"
  },
  {
    "path": "i18n/da/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Filer\nempty-folder = Tom mappe\nempty-folder-hidden = Tom mappe (har skjulte filer)\nno-results = Ingen resultater\nfilesystem = Filsystem\nhome = Hjem\nnetworks = Netværk\nnotification-in-progress = Filoperationer er igangværende.\ntrash = Skraldespand\nrecents = Seneste\nundo = Fortryd\ntoday = I dag\n# Desktop view options\ndesktop-view-options = Indstillinger for skrivebordsudseende...\nshow-on-desktop = Vis på Skrivebordet\ndesktop-folder-content = Indhold på Skrivebordet\nmounted-drives = Monterede drev\ntrash-folder-icon = Skraldespandsikon\nicon-size-and-spacing = Ikonstørrelse og afstand\nicon-size = Ikonstørrelse\n# List view\nname = Navn\nmodified = Ændret\ntrashed-on = Smidt ud\nsize = Størrelse\n# Progress footer\ndetails = Detaljer\ndismiss = Afvis besked\noperations-running = { $running } operationer er i gang ({ $percent }%)...\noperations-running-finished = { $running } operationer er i gang ({ $percent }%), { $finished } færdiggjort...\npause = Pause\nresume = Fortsæt\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Opret arkiv\n\n## Empty Trash Dialog\n\nempty-trash = Tøm skraldespand\nempty-trash-warning = Er du sikker på du vil slette alle objekter i Skraldespanden permanent?\n\n## Mount Error Dialog\n\nmount-error = Kan ikke tilgå drev\n\n## New File/Folder Dialog\n\ncreate-new-file = Opret ny fil\ncreate-new-folder = Opret ny mappe\nfile-name = Filnavn\nfolder-name = Mappenavn\nfile-already-exists = En fil med det navn eksisterer allerede.\nfolder-already-exists = En mappe med det navn eksisterer allerede.\nname-hidden = Navne begyndende med \".\" vil blive skjult.\nname-invalid = Navn kan ikke være \"{ $filename }\".\nname-no-slashes = Navn kan ikke indeholde skråstreg.\n\n## Open/Save Dialog\n\ncancel = Annullér\ncreate = Opret\nopen = Åbn\nopen-file = Åbn fil\nopen-folder = Åbn mappe\nopen-in-new-tab = Åbn i ny fane\nopen-in-new-window = Åbn i nyt vindue\nopen-item-location = Åbn placering for objekt\nopen-multiple-files = Åbn flere filer\nopen-multiple-folders = Åbn flere mapper\nsave = Gem\nsave-file = Gem fil\n\n## Open With Dialog\n\nopen-with-title = Hvordan vil du åbne \"{ $name }\"?\nbrowse-store = Gennemse { $store }\n\n## Rename Dialog\n\nrename-file = Omdøb fil\nrename-folder = Omdøb mappe\n\n## Replace Dialog\n\nreplace = Erstat\nreplace-title = \"{ $filename }\" eksisterer allerede på denne placering.\nreplace-warning = Vil du erstatte den med den du er ved at gemme? Hvis du erstatter den, overskriver du dens indhold.\nreplace-warning-operation = Vil du erstatte den? Hvis du erstatter den, overskriver du dens indhold.\noriginal-file = Original fil\nreplace-with = Erstat med\napply-to-all = Anvend for alle\nkeep-both = Behold begge\nskip = Spring over\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Indstil som eksekverbar fil og start\nset-executable-and-launch-description = Vil du indstille \"{ $name }\" som en eksekverbar fil og starte den?\nset-and-launch = Indstil og start\n\n## Metadata Dialog\n\nowner = Ejer\ngroup = Gruppe\nother = Andet\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Tilføj netværksdrev\nconnect = Opret forbindelse\nconnect-anonymously = Opret forbindelse anonymt\nconnecting = Opretter forbindelse...\ndomain = Domæne\nenter-server-address = Indtast serveradresse\nnetwork-drive-description =\n    Server-adresser inkluderer et protokolpræfiks og adresse.\n    Eksempler: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Tilgængelige protokoller,Præfiks\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDav,dav:// or davs://\nnetwork-drive-error = Kan ikke tilgå netværksdrev\npassword = Adgangskode\nremember-password = Husk adgangskode\ntry-again = Forsøg igen\nusername = Brugernavn\n\n## Operations\n\ncancelled = Annulleret\nedit-history = Redigér historie\nhistory = Historie\nno-history = Ingen historik.\npending = Afventer\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, annulleret\nprogress-paused = { $percent }%, sat på pause\nfailed = Mislykkedes\ncomplete = Færdigt\ncompressing =\n    Komprimerer { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\ncompressed =\n    Komprimeret { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\ncopy_noun = Kopi\ncreating = Opretter \"{ $name }\" i \"{ $parent }\"\ncreated = Oprettet \"{ $name }\" i \"{ $parent }\"\ncopying =\n    Kopierer { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\ncopied =\n    Kopieret { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\nemptying-trash = Tømmer { trash } ({ $progress })...\nemptied-trash = Tømt { trash }\nextracting =\n    Udpakker { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\nextracted =\n    Udpakket { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\nsetting-executable-and-launching = Indstiller \"{ $name }\" som eksekverbar fil og starter\nset-executable-and-launched = Indstillet \"{ $name }\" som eksekverbar fil og startet\nmoving =\n    Flytter { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\nmoved =\n    Flyttet { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\nrenaming = Omdøber \"{ $from }\" til \"{ $to }\"\nrenamed = Omdøbt \"{ $from }\" til \"{ $to }\"\nrestoring =\n    Genopretter { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { trash } ({ $progress })...\nrestored =\n    Genoprettet { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { trash }\nunknown-folder = ukendt mappe\n\n## Open with\n\nmenu-open-with = Åbn med...\ndefault-app = { $name } (standardindstilling)\n\n## Show details\n\nshow-details = Vis detaljer\ntype = Type: { $mime }\nitems = Objekter: { $items }\nitem-size = Størrelse: { $size }\nitem-created = Oprettet: { $created }\nitem-modified = Ændret: { $modified }\nitem-accessed = Tilgået: { $accessed }\ncalculating = Beregner...\n\n## Settings\n\nsettings = Indstillinger\n\n### Appearance\n\nappearance = Udseende\ntheme = Tema\nmatch-desktop = Match skrivebord\ndark = Mørk\nlight = Lys\n# Context menu\nadd-to-sidebar = Tilføj til sidebjælke\ncompress = Komprimér\nextract-here = Extract\nnew-file = Ny fil...\nnew-folder = Ny mappe...\nopen-in-terminal = Åbn i terminal\nmove-to-trash = Flyt til skraldespand\nrestore-from-trash = Genopret fra skraldespand\nremove-from-sidebar = Fjern fra sidebjælke\nsort-by-name = Sortér efter navn\nsort-by-modified = Sortér efter ændret\nsort-by-size = Sortér efter størrelse\nsort-by-trashed = Sortér efter sletningsdato\n\n## Desktop\n\nchange-wallpaper = Skift baggrundsbillede...\ndesktop-appearance = Skrivebordsudseende...\ndisplay-settings = Skærmindstillinger...\n\n# Menu\n\n\n## File\n\nfile = Fil\nnew-tab = Ny fane\nnew-window = Nyt vindue\nrename = Omdøb...\nclose-tab = Luk fane\nquit = Afslut\n\n## Edit\n\nedit = Redigér\ncut = Klip\ncopy = Kopiér\npaste = Sæt ind\nselect-all = Vælg alt\n\n## View\n\nzoom-in = Zoom ind\ndefault-size = Standardstørrelse\nzoom-out = Zoom ud\nview = Visning\ngrid-view = Gitter-visning\nlist-view = Liste-visning\nshow-hidden-files = Vis skjulte filer\nlist-directories-first = List mapper først\ngallery-preview = Galleri-forhåndsvisning\nmenu-settings = Indstillinger...\nmenu-about = Om COSMIC Filer...\n\n## Sort\n\nsort = Sortér\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Nyeste først\nsort-oldest-first = Ældste først\nsort-smallest-to-largest = Mindste til største\nsort-largest-to-smallest = Største til mindste\n"
  },
  {
    "path": "i18n/de/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Dateien\nempty-folder = Leerer Ordner\nempty-folder-hidden = Leerer Ordner (hat versteckte Elemente)\nno-results = Keine Ergebnisse gefunden\nfilesystem = Dateisystem\nhome = Benutzerordner\nnetworks = Netzwerke\nnotification-in-progress = Dateivorgänge werden ausgeführt\ntrash = Papierkorb\nrecents = Zuletzt verwendet\nundo = Rückgängig\ntoday = Heute\n# Optionen für die Desktop-Ansicht\ndesktop-view-options = Optionen für die Desktop-Ansicht…\nshow-on-desktop = Auf Desktop anzeigen\ndesktop-folder-content = Inhalt des Desktop-Ordners\nmounted-drives = Eingehängte Laufwerke\ntrash-folder-icon = Ordnersymbol des Papierkorbs\nicon-size-and-spacing = Symbolgröße und -abstand\nicon-size = Symbolgröße\ngrid-spacing = Rasterabstand\n# Listenansicht\nname = Name\nmodified = Geändert\ntrashed-on = In den Papierkorb verschoben\nsize = Größe\n# Fortschrittsfußzeile\ndetails = Details\ndismiss = Meldung verwerfen\noperations-running =\n    { $running } { $running ->\n        [one] laufender Vorgang\n       *[other] laufende Vorgänge\n    } ({ $percent } %)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] laufender Vorgang\n       *[other] laufende Vorgänge\n    } ({ $percent } %), { $finished } abgeschlossen...\npause = Pause\nresume = Fortsetzen\n\n# Dialoge\n\n\n## Komprimieren-Dialog\n\ncreate-archive = Archiv erstellen\n\n## Entpacken-Dialog\n\nextract-password-required = Passwort erforderlich\nextract-to = Entpacken nach...\nextract-to-title = In Ordner entpacken\n\n## Dialog zum Leeren des Papierkorbs\n\nempty-trash = Papierkorb leeren\nempty-trash-warning = Elemente im Papierkorb werden endgültig gelöscht\n\n## Einhängefehler-Dialog\n\nmount-error = Zugriff auf Laufwerk nicht möglich\n# Neue(r) Datei/Ordner-Dialog\ncreate-new-file = Neue Datei erstellen\ncreate-new-folder = Neuen Ordner erstellen\nfile-name = Dateiname\nfolder-name = Ordnername\nfile-already-exists = Eine Datei mit diesem Namen existiert bereits\nfolder-already-exists = Ein Ordner mit diesem Namen existiert bereits\nname-hidden = Mit „.“ beginnende Namen werden versteckt\nname-invalid = Name darf nicht „{ $filename }“ sein\nname-no-slashes = Name darf keine Schrägstriche enthalten\n# Öffnen/Speichern-Dialog\ncancel = Abbrechen\ncreate = Erstellen\nopen = Öffnen\nopen-file = Datei öffnen\nopen-folder = Ordner öffnen\nopen-in-new-tab = In neuem Tab öffnen\nopen-in-new-window = In neuem Fenster öffnen\nopen-item-location = Speicherort des Elements öffnen\nopen-multiple-files = Mehrere Dateien öffnen\nopen-multiple-folders = Mehrere Ordner öffnen\nsave = Speichern\nsave-file = Datei speichern\n\n## Öffnen-mit-Dialog\n\nopen-with-title = Wie möchtest du „{ $name }“ öffnen?\nbrowse-store = { $store } durchsuchen\nother-apps = Andere Anwendungen\nrelated-apps = Ähnliche Anwendungen\n\n## Endgültig-löschen-Dialog\n\nselected-items = Die { $items } ausgewählten Elemente\npermanently-delete-question = Endgültig löschen?\ndelete = Löschen\npermanently-delete-warning = { $target } wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.\n# Umbenennen-Dialog\nrename-file = Datei umbenennen\nrename-folder = Ordner umbenennen\n# Ersetzen-Dialog\nreplace = Ersetzen\nreplace-title = „{ $filename }“ existiert bereits an diesem Ort\nreplace-warning = Möchtest du sie durch diejenige ersetzen, die du gerade speicherst? Beim Ersetzen wird ihr Inhalt überschrieben.\nreplace-warning-operation = Möchtest du sie ersetzen? Beim Ersetzen wird ihr Inhalt überschrieben.\noriginal-file = Originaldatei\nreplace-with = Ersetzen mit\napply-to-all = Auf alle anwenden\nkeep-both = Beide behalten\nskip = Überspringen\n\n## Dialog zum Festlegen als ausführbar und starten\n\nset-executable-and-launch = Als ausführbar festlegen und starten\nset-executable-and-launch-description = Möchtest du „{ $name }“ als ausführbar festlegen und starten?\nset-and-launch = Festlegen und starten\n\n## Metadaten-Dialog\n\nopen-with = Öffnen mit\nowner = Eigentümer\ngroup = Gruppe\nother = Andere\n\n### Modus 0\n\nnone = Keine\n\n### Modus 1 (ungewöhnlich)\n\nexecute-only = Nur ausführen\n\n### Modus 2 (ungewöhnlich)\n\nwrite-only = Nur schreiben\n\n### Modus 3 (ungewöhnlich)\n\nwrite-execute = Schreiben und ausführen\n\n### Modus 4\n\nread-only = Nur lesen\n\n### Modus 5\n\nread-execute = Lesen und ausführen\n\n### Modus 6\n\nread-write = Lesen und schreiben\n\n### Modus 7\n\nread-write-execute = Lesen, schreiben und ausführen\n\n## Fehlerdialog zum gewünschten Pfad\n\nfavorite-path-error = Fehler beim Öffnen des Verzeichnisses\nfavorite-path-error-description =\n    „{ $path }“ kann nicht geöffnet werden\n    „{ $path }“ existiert möglicherweise nicht oder du hast keine Berechtigung zum Öffnen\n\n    Möchtest du es aus der Seitenleiste entfernen?\nremove = Entfernen\nkeep = Behalten\n\n# Kontextseiten\n\n\n## Über\n\n\n## Netzlaufwerk hinzufügen\n\nadd-network-drive = Netzlaufwerk hinzufügen\nconnect = Verbinden\nconnect-anonymously = Anonym verbinden\nconnecting = Wird verbunden...\ndomain = Domain\nenter-server-address = Serveradresse eingeben\nnetwork-drive-description =\n    Serveradressen enthalten ein Protokollpräfix und eine Adresse.\n    Beispiele: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Achte darauf, dass das Komma, das die Spalten trennt, erhalten bleibt\n\nnetwork-drive-schemes =\n    Verfügbare Protokolle,Präfix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// oder ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// oder ssh://\n    WebDav,dav:// oder davs://\nnetwork-drive-error = Zugriff auf Netzlaufwerk nicht möglich\npassword = Passwort\nremember-password = Passwort merken\ntry-again = Erneut versuchen\nusername = Benutzername\n\n## Vorgänge\n\ncancelled = Abgebrochen\nedit-history = Verlauf bearbeiten\nhistory = Verlauf\nno-history = Keine Einträge im Verlauf.\npending = Ausstehend\nprogress = { $percent } %\nprogress-cancelled = { $percent } %, abgeschlossen\nprogress-paused = { $percent } %, pausiert\nfailed = Fehlgeschlagen\ncomplete = Abgeschlossen\ncompressing =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } von „{ $from }“ nach „{ $to }“ komprimiert ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } von „{ $from }“ nach „{ $to }“ komprimiert\ncopy_noun = Kopie\ncreating = „{ $name }“ in „{ $parent }“ wird erstellt\ncreated = „{ $name }“ in „{ $parent }“ wurde erstellt\ncopying =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } von „{ $from }“ nach „{ $to }“ kopiert ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } „{ $from }“ nach „{ $to }“ kopiert\ndeleting =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } aus dem { trash } gelöscht ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } aus dem { trash } gelöscht\nemptying-trash = { trash } wird geleert ({ $progress })...\nemptied-trash = { trash } geleert\nextracting =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } von „{ $from }“ nach „{ $to }“ entpackt ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } von „{ $from }“ nach „{ $to }“ entpackt\nsetting-executable-and-launching = „{ $name }“ wird als ausführbar festgelegt und gestartet\nset-executable-and-launched = „{ $name }“ als ausführbar festgelegt und gestartet\nsetting-permissions = Berechtigungen für „{ $name }“ werden auf { $mode } festgelegt\nset-permissions = Berechtigungen für „{ $name }“ auf { $mode } festlegen\nmoving =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } von „{ $from }“ nach „{ $to }“ verschoben ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } von „{ $from }“ nach „{ $to }“ verschoben\npermanently-deleting =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } endgültig gelöscht\npermanently-deleted =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Element wurden\n    } endgültig gelöscht\nrenaming = „{ $from }“ wird in „{ $to }“ umbenannt\nrenamed = „{ $from }“ wurde in „{ $to }“ umbenannt\nrestoring =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } aus dem { trash } wiederhergestellt ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } aus dem { trash } wiederhergestellt\nunknown-folder = unbekannter Ordner\n\n## Öffnen mit\n\nmenu-open-with = Öffnen mit...\ndefault-app = { $name } (Standard)\n\n## Details anzeigen\n\nshow-details = Details anzeigen\ntype = Typ: { $mime }\nitems = Elemente: { $items }\nitem-size = Größe: { $size }\nitem-created = Erstellt: { $created }\nitem-modified = Geändert: { $modified }\nitem-accessed = Zugegriffen: { $accessed }\ncalculating = Wird berechnet...\n\n## Einstellungen\n\nsettings = Einstellungen\nsingle-click = Mit einem Klick öffnen\n\n### Aussehen\n\nappearance = Aussehen\ntheme = Thema\nmatch-desktop = An Desktop anpassen\ndark = Dunkel\nlight = Hell\n\n### Zum Suchen tippen\n\ntype-to-search = Zum Suchen tippen\ntype-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner\ntype-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein\n# Kontextmenü\nadd-to-sidebar = Zur Seitenleiste hinzufügen\ncompress = Komprimieren...\ndelete-permanently = Endgültig löschen\nextract-here = Entpacken\nnew-file = Neue Datei...\nnew-folder = Neuer Ordner...\nopen-in-terminal = Im Terminal öffnen\nmove-to-trash = In den Papierkorb verschieben\nrestore-from-trash = Aus dem Papierkorb wiederherstellen\nremove-from-sidebar = Von der Seitenleiste entfernen\nsort-by-name = Nach Name sortieren\nsort-by-modified = Nach Änderung sortieren\nsort-by-size = Nach Größe sortieren\nsort-by-trashed = Nach Löschzeitpunkt sortieren\n\n## Desktop\n\nchange-wallpaper = Hintergrundbild ändern...\ndesktop-appearance = Desktop-Aussehen...\ndisplay-settings = Anzeigeeinstellungen...\n\n# Menü\n\n\n## Datei\n\nfile = Datei\nnew-tab = Neuer Tab\nnew-window = Neues Fenster\nreload-folder = Ordner neu laden\nrename = Umbenennen...\nclose-tab = Tab schließen\nquit = Beenden\n\n## Bearbeiten\n\nedit = Bearbeiten\ncut = Ausschneiden\ncopy = Kopieren\npaste = Einfügen\nselect-all = Alles auswählen\n\n## Ansicht\n\nzoom-in = Hineinzoomen\ndefault-size = Standardgröße\nzoom-out = Herauszoomen\nview = Ansicht\ngrid-view = Rasteransicht\nlist-view = Listenansicht\nshow-hidden-files = Versteckte Dateien anzeigen\nlist-directories-first = Verzeichnisse zuerst auflisten\ngallery-preview = Galerie-Vorschau\nmenu-settings = Einstellungen...\nmenu-about = Über COSMIC Dateien...\n\n## Sortieren\n\nsort = Sortierung\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Neueste zuerst\nsort-oldest-first = Älteste zuerst\nsort-smallest-to-largest = Von klein nach groß\nsort-largest-to-smallest = Von groß nach klein\nrepository = Repository\nempty-trash-title = Papierkorb leeren?\ncopy-to-title = Kopierziel auswählen\ncopy-to-button-label = Kopieren\nsupport = Unterstützung\nprogress-failed = { $percent } %, fehlgeschlagen\nremoving-from-recents =\n    { $items } { $items ->\n        [one] Element wird\n       *[other] Elemente werden\n    } aus { recents } entfernt\nremoved-from-recents =\n    { $items } { $items ->\n        [one] Element wurde\n       *[other] Elemente wurden\n    } aus { recents } entfernt\neject = Auswerfen\ntype-to-search-select = Wählt die erste übereinstimmende Datei oder den ersten übereinstimmenden Ordner aus\npasted-image = Eingefügtes Bild\npasted-text = Eingefügter Text\npasted-video = Eingefügtes Video\ncopy-to = Kopieren nach...\nshow-recents = Ordner zuletzt verwendeter Elemente in der Seitenleiste\nclear-recents-history = Verlauf zuletzt verwendeter Elemente leeren\ncomment = Dateimanager für den COSMIC Desktop\nkeywords = Ordner;Manager;\nmove-to-button-label = Verschieben\nmove-to-title = Verschiebeziel auswählen\nremove-from-recents = Aus den zuletzt verwendeten Elementen entfernen\nmove-to = Verschieben nach...\ncopy-path = Pfad kopieren\nmixed = Gemischt\n"
  },
  {
    "path": "i18n/el/cosmic_files.ftl",
    "content": "empty-folder = Άδειος φάκελος\nno-results = Δεν βρέθηκαν αποτελέσματα\ntrash = Κάδος Ανακύκλωσης\nrecents = Πρόσφατα\ncosmic-files = COSMIC Αρχεία\nempty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά αντικείμενα)\nfilesystem = Σύστημα αρχείων\nhome = Προσωπικός φάκελος\nnetworks = Δίκτυο\ncomment = Αρχεία για το COSMIC περιβάλλον\nkeywords = Φάκελος;Διαχειριστής;\nrename = Μετονομασία...\nclose-tab = Κλείσιμο καρτέλας\nlight = Φωτεινό\ndark = Σκοτεινό\n"
  },
  {
    "path": "i18n/en/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Files\ncomment = File manager for the COSMIC desktop\nkeywords = Folder;Manager;\nempty-folder = Empty folder\nempty-folder-hidden = Empty folder (has hidden items)\nno-results = No results found\nfilesystem = Filesystem\nhome = Home\nnetworks = Networks\nnotification-in-progress = File operations are in progress\ntrash = Trash\nrecents = Recents\nundo = Undo\ntoday = Today\n\n# Desktop view options\ndesktop-view-options = Desktop view options...\nshow-on-desktop = Show on Desktop\ndesktop-folder-content = Desktop folder content\nmounted-drives = Mounted drives\ntrash-folder-icon = Trash folder icon\nicon-size-and-spacing = Icon size and spacing\nicon-size = Icon size\ngrid-spacing = Grid spacing\n\n# List view\nname = Name\nmodified = Modified\ntrashed-on = Trashed\nsize = Size\n\n# Progress footer\ndetails = Details\ndismiss = Dismiss message\noperations-running = {$running} {$running ->\n    [one] operation\n    *[other] operations\n  } running ({$percent}%)...\noperations-running-finished = {$running} {$running ->\n    [one] operation\n    *[other] operations\n  } running ({$percent}%), {$finished} finished...\npause = Pause\nresume = Resume\n\n# Dialogs\n\n## Compress Dialog\ncreate-archive = Create archive\n\n## Copy To Dialog\ncopy-to-title = Select copy destination\ncopy-to-button-label = Copy\n\n## Extract Dialog\nextract-password-required = Password required\nextract-to = Extract To...\nextract-to-title = Extract to folder\n\n## Empty Trash Dialog\nempty-trash = Empty trash\nempty-trash-title = Empty trash?\nempty-trash-warning = Items in the Trash folder will be permanently deleted\n\n## Mount Error Dialog\nmount-error = Unable to access drive\n\n## Move To Dialog\nmove-to-title = Select move destination\nmove-to-button-label = Move\n\n## New File/Folder Dialog\ncreate-new-file = Create new file\ncreate-new-folder = Create new folder\nfile-name = File name\nfolder-name = Folder name\nfile-already-exists = A file with that name already exists\nfolder-already-exists = A folder with that name already exists\nname-hidden = Names starting with \".\" will be hidden\nname-invalid = Name cannot be \"{$filename}\"\nname-no-slashes = Name cannot contain slashes\n\n## Open/Save Dialog\ncancel = Cancel\ncreate = Create\nopen = Open\nopen-file = Open file\nopen-folder = Open folder\nopen-in-new-tab = Open in new tab\nopen-in-new-window = Open in new window\nopen-item-location = Open item location\nopen-multiple-files = Open multiple files\nopen-multiple-folders = Open multiple folders\nsave = Save\nsave-file = Save file\n\n## Open With Dialog\nopen-with-title = How do you want to open \"{$name}\"?\nbrowse-store = Browse {$store}\nother-apps = Other applications\nrelated-apps = Related applications\ncontext-action = Context action\ncontext-action-confirm-title = Run \"{$name}\"?\ncontext-action-confirm-warning = This will run on {$items} {$items ->\n    [one] item\n    *[other] items\n  }.\nrun = Run\n\n## Permanently delete Dialog\nselected-items = The {$items} selected items\npermanently-delete-question = Permanently delete?\ndelete = Delete\npermanently-delete-warning = {$target} will be permanently deleted. This action can't be undone.\n\n## Rename Dialog\nrename-file = Rename file\nrename-folder = Rename folder\n\n## Replace Dialog\nreplace = Replace\nreplace-title = \"{$filename}\" already exists in this location\nreplace-warning = Do you want to replace it with the one you are saving? Replacing it will overwrite its content.\nreplace-warning-operation = Do you want to replace it? Replacing it will overwrite its content.\noriginal-file = Original file\nreplace-with = Replace with\napply-to-all = Apply to all\nkeep-both = Keep both\nskip = Skip\n\n## Set as Executable and Launch Dialog\nset-executable-and-launch = Set as executable and launch\nset-executable-and-launch-description = Do you want to set \"{$name}\" as executable and launch it?\nset-and-launch = Set and launch\n\n## Metadata Dialog\nopen-with = Open with\nowner = Owner\ngroup = Group\nother = Other\nmixed = Mixed\n### Mode 0\nnone = None\n### Mode 1 (unusual)\nexecute-only = Execute-only\n### Mode 2 (unusual)\nwrite-only = Write-only\n### Mode 3 (unusual)\nwrite-execute = Write and execute\n### Mode 4\nread-only = Read-only\n### Mode 5\nread-execute = Read and execute\n### Mode 6\nread-write = Read and write\n### Mode 7\nread-write-execute = Read, write, and execute\n\n## Favorite Path Error Dialog\nfavorite-path-error = Error opening directory\nfavorite-path-error-description =\n    Unable to open \"{$path}\"\n    \"{$path}\" might not exist or you might not have permission to open it\n\n    Would you like to remove it from the sidebar?\nremove = Remove\nkeep = Keep\n\n# Context Pages\n\n## About\nrepository = Repository\nsupport = Support\n\n## Add Network Drive\nadd-network-drive = Add network drive\nconnect = Connect\nconnect-anonymously = Connect anonymously\nconnecting = Connecting...\ndomain = Domain\nenter-server-address = Enter server address\nnetwork-drive-description =\n    Server addresses include a protocol prefix and address.\n    Examples: ssh://192.168.0.1, ftp://[2001:db8::1]\n### Make sure to keep the comma which separates the columns\nnetwork-drive-schemes =\n    Available protocols,Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDAV,dav:// or davs://\nnetwork-drive-error = Unable to access network drive\npassword = Password\nremember-password = Remember password\ntry-again = Try again\nusername = Username\n\n## Operations\ncancelled = Cancelled\nedit-history = Edit history\nhistory = History\nno-history = No items in history.\npending = Pending\nprogress = {$percent}%\nprogress-cancelled = {$percent}%, cancelled\nprogress-failed = {$percent}%, failed\nprogress-paused = {$percent}%, paused\nfailed = Failed\ncomplete = Complete\ncompressing = Compressing {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\" ({$progress})...\ncompressed = Compressed {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\"\ncopy_noun = Copy\npasted-image = Pasted Image\npasted-text = Pasted Text\npasted-video = Pasted Video\ncreating = Creating \"{$name}\" in \"{$parent}\"\ncreated = Created \"{$name}\" in \"{$parent}\"\ncopying = Copying {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\" ({$progress})...\ncopied = Copied {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\"\ndeleting = Deleting {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {trash} ({$progress})...\ndeleted = Deleted {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {trash}\nemptying-trash = Emptying {trash} ({$progress})...\nemptied-trash = Emptied {trash}\nextracting = Extracting {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\" ({$progress})...\nextracted = Extracted {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\"\nsetting-executable-and-launching = Setting \"{$name}\" as executable and launching\nset-executable-and-launched = Set \"{$name}\" as executable and launched\nsetting-permissions = Setting permissions for \"{$name}\" to {$mode}\nset-permissions = Set permissions for \"{$name}\" to {$mode}\nmoving = Moving {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\" ({$progress})...\nmoved = Moved {$items} {$items ->\n        [one] item\n        *[other] items\n    } from \"{$from}\" to \"{$to}\"\npermanently-deleting = Permanently deleting {$items} {$items ->\n        [one] item\n        *[other] items\n    }\npermanently-deleted = Permanently deleted {$items} {$items ->\n        [one] item\n        *[other] items\n    }\nremoving-from-recents = Removing {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {recents}\nremoved-from-recents = Removed {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {recents}\nrenaming = Renaming \"{$from}\" to \"{$to}\"\nrenamed = Renamed \"{$from}\" to \"{$to}\"\nrestoring = Restoring {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {trash} ({$progress})...\nrestored = Restored {$items} {$items ->\n        [one] item\n        *[other] items\n    } from {trash}\nunknown-folder = unknown folder\n\n## Open with\nmenu-open-with = Open with...\ndefault-app = {$name} (default)\n\n## Show details\nshow-details = Show details\ntype = Type: {$mime}\nitems = Items: {$items}\nitem-size = Size: {$size}\nitem-created = Created: {$created}\nitem-modified = Modified: {$modified}\nitem-accessed = Accessed: {$accessed}\ncalculating = Calculating...\n\n## Settings\nsettings = Settings\nsingle-click = Single click to open\nshow-recents = Recents folder in the sidebar\n\n### Appearance\nappearance = Appearance\ntheme = Theme\nmatch-desktop = Match desktop\ndark = Dark\nlight = Light\n\n### Type to search\ntype-to-search = Type to search\ntype-to-search-recursive = Searches the current folder and all subfolders\ntype-to-search-enter-path = Enters the path to the directory or file\ntype-to-search-select = Selects the first matching file or folder\n\n# Context menu\nadd-to-sidebar = Add to sidebar\nclear-recents-history = Clear Recents history\ncompress = Compress...\ncopy-to = Copy to...\ndelete-permanently = Delete permanently\neject = Eject\nextract-here = Extract\nnew-file = New file...\nnew-folder = New folder...\nopen-in-terminal = Open in terminal\nmove-to = Move to...\nmove-to-trash = Move to trash\nrestore-from-trash = Restore from trash\nremove-from-sidebar = Remove from sidebar\nsort-by-name = Sort by name\nsort-by-modified = Sort by modified\nsort-by-size = Sort by size\nsort-by-trashed = Sort by delete time\nremove-from-recents = Remove from recents\n\n## Desktop\nchange-wallpaper = Change wallpaper...\ndesktop-appearance = Desktop appearance...\ndisplay-settings = Display settings...\n\n# Menu\n\n## File\nfile = File\nnew-tab = New tab\nnew-window = New window\nreload-folder = Reload folder\nrename = Rename...\nclose-tab = Close tab\nquit = Quit\n\n## Edit\nedit = Edit\ncut = Cut\ncopy = Copy\ncopy-path = Copy path\npaste = Paste\nselect-all = Select all\n\n## View\nzoom-in = Zoom in\ndefault-size = Default size\nzoom-out = Zoom out\nview = View\ngrid-view = Grid view\nlist-view = List view\nshow-hidden-files = Show hidden files\nlist-directories-first = List directories first\ngallery-preview = Gallery preview\nmenu-settings = Settings...\nmenu-about = About COSMIC Files...\n\n## Sort\nsort = Sort\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Newest first\nsort-oldest-first = Oldest first\nsort-smallest-to-largest = Smallest to largest\nsort-largest-to-smallest = Largest to smallest\n"
  },
  {
    "path": "i18n/en-GB/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/eo/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/es/cosmic_files.ftl",
    "content": "cosmic-files = Archivos COSMIC\ncomment = Gestor de archivos de COSMIC\nkeywords = Archivos;Ficheros;Gestor;Explorador;\nempty-folder = Carpeta vacía\nempty-folder-hidden = Carpeta vacía (Contiene archivos ocultos)\nno-results = No se encontraron resultados\nfilesystem = Sistema de archivos\nhome = Inicio\nnetworks = Redes\nnotification-in-progress = Las operaciones de archivo están en progreso.\ntrash = Papelera\nrecents = Reciente\nundo = Deshacer\ntoday = Hoy\n# Desktop view options\ndesktop-view-options = Opciones de vista del escritorio...\nshow-on-desktop = Mostrar en el escritorio\ndesktop-folder-content = Contenido de la carpeta del escritorio\nmounted-drives = Unidades montadas\ntrash-folder-icon = Icono de la papelera\nicon-size-and-spacing = Tamaño y espaciado de los iconos\nicon-size = Tamaño del icono\ngrid-spacing = Espaciado de la cuadrícula\n# List view\nname = Nombre\nmodified = Modificado\ntrashed-on = Enviado a la papelera\nsize = Tamaño\n# Progress footer\ndetails = Detalles\ndismiss = Descartar mensaje\noperations-running =\n    { $running } { $running ->\n        [one] operación\n       *[other] operaciones\n    } ejecutándose ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operación\n       *[other] operaciones\n    } ejecutándose ({ $percent }%), { $finished } finished...\npause = Pausar\nresume = Reanudar\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Crear archivo\n\n## Extract Dialog\n\nextract-password-required = Contraseña requerida\nextract-to = Extraer en...\nextract-to-title = Extraer a una carpeta\n\n## Empty Trash Dialog\n\nempty-trash = Vaciar la papelera\nempty-trash-warning = ¿Está seguro de que quiere eliminar permanentemente todos los archivos de la papelera?\n\n## Mount Error Dialog\n\nmount-error = No se puede acceder a la unidad\n\n## New File/Folder Dialog\n\ncreate-new-file = Crear nuevo archivo\ncreate-new-folder = Crear nueva carpeta\nfile-name = Nombre del archivo\nfolder-name = Nombre de la carpeta\nfile-already-exists = Ya existe un archivo con ese nombre.\nfolder-already-exists = Ya existe una carpeta con ese nombre.\nname-hidden = Nombres comenzando con \".\" serán ocultados.\nname-invalid = El nombre no puede ser: \"{ $filename }\".\nname-no-slashes = El nombre no puede contener slashes (barras).\n\n## Open/Save Dialog\n\ncancel = Cancelar\ncreate = Crear\nopen = Abrir\nopen-file = Abrir archivo\nopen-folder = Abrir carpeta\nopen-in-new-tab = Abrir en nueva pestaña\nopen-in-new-window = Abrir en nueva ventana\nopen-item-location = Abrir ubicación del archivo\nopen-multiple-files = Abrir multiples archivos\nopen-multiple-folders = Abrir multiples carpetas\nsave = Guardar\nsave-file = Guardar archivo\n\n## Open With Dialog\n\nopen-with-title = ¿Cómo quiere abrir \"{ $name }\"?\nbrowse-store = Explorar { $store }\nother-apps = Otras aplicaciones\nrelated-apps = Aplicaciones relacionadas\n\n## Permanently delete Dialog\n\nselected-items = los { $items } archivos seleccionados\npermanently-delete-question = Eliminar permanentemente\ndelete = Eliminar\npermanently-delete-warning = ¿Quiere eliminar permanentemente { $target }? Esta acción no se puede deshacer.\n\n## Rename Dialog\n\nrename-file = Renombrar archivo\nrename-folder = Renombrar carpeta\n\n## Replace Dialog\n\nreplace = Reemplazar\nreplace-title = { $filename } ya existe en esta ruta.\nreplace-warning = ¿Quiere remplazarlo con el que está guardando? Reemplazarlo sobrescribirá su contenido.\nreplace-warning-operation = ¿Quieres reemplazarlo? Reemplazarlo sobrescribirá su contenido.\noriginal-file = Archivo original\nreplace-with = Reemplazar con\napply-to-all = Aplicar a todos\nkeep-both = Conservar ambos\nskip = Saltar\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Establecer como ejecutable y ejecutar\nset-executable-and-launch-description = ¿Quieres establecer \"{ $name }\" como ejecutable y ejecutar?\nset-and-launch = Establecer y ejecutar\n\n## Metadata Dialog\n\nopen-with = Abrir con\nowner = Propietario\ngroup = Grupo\nother = Otro\n\n### Mode 0\n\nnone = Ninguno\n\n### Mode 1 (unusual)\n\nexecute-only = Únicamente ejecución\n\n### Mode 2 (unusual)\n\nwrite-only = Únicamente escritura\n\n### Mode 3 (unusual)\n\nwrite-execute = Escritura y ejecución\n\n### Mode 4\n\nread-only = Únicamente lectura\n\n### Mode 5\n\nread-execute = Lectura y ejecución\n\n### Mode 6\n\nread-write = Lectura y escritura\n\n### Mode 7\n\nread-write-execute = Lectura, escritura y ejecución\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Error al abrir el directorio\nfavorite-path-error-description =\n    No se puede abrir \"{ $path }\".\n    Puede que no exista o que no tenga permisos para abrirlo.\n\n    ¿Quiere eliminarlo de la barra lateral?\nremove = Eliminar\nkeep = Mantener\n\n# Context Pages\n\n\n## About\n\nrepository = Repositorio\nsupport = Apoyo\n\n## Add Network Drive\n\nadd-network-drive = Agregar una unidad de red\nconnect = Conectar\nconnect-anonymously = Conectar de forma anónima\nconnecting = Conectando...\ndomain = Dominio\nenter-server-address = Ingresa la dirección del servidor\nnetwork-drive-description =\n    Las direcciones de los servidores incluyen un prefijo de protocolo y una dirección.\n    Ejemplos: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocolos disponibles,Prefijo\n    AppleTalk,afp://\n    Protocolo de transferencia de archivos,ftp:// o ftps://\n    Sistema de archivos de red,nfs://\n    Bloque de mensajes del servidor,smb://\n    Protocolo de transferencia de archivos SSH,sftp:// o ssh://\n    WebDav,dav:// o davs://\nnetwork-drive-error = No se puede acceder a la unidad de red\npassword = Contraseña\nremember-password = Recordar contraseña\ntry-again = Intentar de nuevo\nusername = Nombre de usuario\n\n## Operations\n\ncancelled = Cancelada\nedit-history = Historial de ediciones\nhistory = Historial\nno-history = No hay archivos en el historial.\npending = Pendiente\nprogress = { $percent } %\nprogress-cancelled = { $percent }%, cancelado\nprogress-paused = { $percent }%, pausado\nfailed = Fallidas\ncomplete = Completadas\ncompressing =\n    Comprimiendo { $items ->\n        [one] elemento\n       *[other] { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items ->\n        [one] Se ha comprimido un elemento\n       *[other] Se han comprimidos { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\ncopy_noun = Copia\ncreating = Creando \"{ $name }\" en \"{ $parent }\"\ncreated = Se ha creado \"{ $name }\" en \"{ $parent }\"\ncopying =\n    Copiando { $items ->\n        [one] elemento\n       *[other] { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncopied =\n    { $items ->\n        [one] Se ha copiado un archivo\n       *[other] Se han copiado { $items } archivos\n    } de \"{ $from }\" a \"{ $to }\"\ndeleting =\n    { $items ->\n        [one] Eliminando un archivo\n       *[other] Eliminando { $items } archivos\n    } de la { trash } ({ $progress })...\ndeleted =\n    { $items ->\n        [one] Se ha eliminado un elemento\n       *[other] Se han eliminado { $items } elementos\n    } de la { trash }\nemptying-trash = Vaciando la { trash } ({ $progress })...\nemptied-trash = Se ha vaciado la { trash }\nextracting =\n    Extrayendo { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nextracted =\n    { $items ->\n        [one] Se ha extraído un elemento\n       *[other] Se han extraído { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\nsetting-executable-and-launching = Estableciendo \"{ $name }\" como ejecutable y lanzando\nset-executable-and-launched = Se ha establecido \"{ $name }\" como ejecutable y se ha lanzado\nmoving =\n    Moviendo { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nmoved =\n    { $items ->\n        [one] Se ha movido un elemento\n       *[other] Se han movido { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\npermanently-deleting =\n    Eliminando { $items } { $items ->\n        [one] elemento\n       *[other] archivos\n    } permanentemente\npermanently-deleted =\n    { $items ->\n        [one] Se ha eliminado un archivo permanentemente\n       *[other] Se han eliminado { $items } archivos permanentemente\n    }\nrenaming = Cambiando el nombre de \"{ $from }\" a \"{ $to }\"\nrenamed = Se ha cambiado el nombre de \"{ $from }\" a \"{ $to }\"\nrestoring =\n    Restaurando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de la { trash } ({ $progress })...\nrestored =\n    { $items ->\n        [one] Se ha restaurado un archivo\n       *[other] Se han restaurado { $items } archivos\n    } de la { trash }\nunknown-folder = carpeta desconocida\n\n## Open with\n\nmenu-open-with = Abrir con...\ndefault-app = { $name } (predeterminado)\n\n## Show details\n\nshow-details = Mostrar detalles\ntype = Tipo: { $mime }\nitems = Archivos: { $items }\nitem-size = Tamaño: { $size }\nitem-created = Fecha de creación: { $created }\nitem-modified = Última modificación: { $modified }\nitem-accessed = Último acceso: { $accessed }\ncalculating = Calculando...\n\n## Settings\n\nsettings = Configuración\nsingle-click = Abrir con solo un clic\n\n### Appearance\n\nappearance = Apariencia\ntheme = Tema\nmatch-desktop = Seguir el estilo del escritorio\ndark = Oscuro\nlight = Claro\n\n### Type to Search\n\ntype-to-search = Escriba para buscar\ntype-to-search-recursive = Buscar en la carpeta actual y todas sus subcarpetas\ntype-to-search-enter-path = Escriba la ruta del directorio o archivo\n# Context menu\nadd-to-sidebar = Añadir a la barra lateral\ncompress = Comprimir\ndelete-permanently = Eliminar permanentemente\neject = Expulsar\nextract-here = Extraer aquí\nnew-file = Nuevo archivo...\nnew-folder = Nueva carpeta...\nopen-in-terminal = Abrir en la consola\nmove-to-trash = Mover a la papelera\nrestore-from-trash = Restaurar de la papelera\nremove-from-sidebar = Quitar de la barra lateral\nsort-by-name = Ordenar por nombre\nsort-by-modified = Ordenar por fecha de modificación\nsort-by-size = Ordenar por tamaño\nsort-by-trashed = Ordenar por fecha de eliminación\nremove-from-recents = Quitar de recientes\n\n## Desktop\n\nchange-wallpaper = Cambiar fondo de pantalla...\ndesktop-appearance = Apariencia del escritorio...\ndisplay-settings = Configuración de pantalla...\n\n# Menu\n\n\n## File\n\nfile = Archivo\nnew-tab = Nueva pestaña\nnew-window = Nueva ventana\nreload-folder = Refrescar carpeta\nrename = Renombrar...\nclose-tab = Cerrar pestaña\nquit = Cerrar\n\n## Edit\n\nedit = Editar\ncut = Cortar\ncopy = Copiar\npaste = Pegar\nselect-all = Seleccionar todo\n\n## View\n\nzoom-in = Ampliar\ndefault-size = Tamaño predeterminado\nzoom-out = Disminuir\nview = Vista\ngrid-view = Vista de cuadrícula\nlist-view = Vista de lista\nshow-hidden-files = Mostrar archivos ocultos\nlist-directories-first = Enumerar los directorios primero\ngallery-preview = Vista previa de la galería\nmenu-settings = Configuración...\nmenu-about = Acerca de COSMIC Files...\n\n## Sort\n\nsort = Ordenar\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Más reciente primero\nsort-oldest-first = Más antiguo primero\nsort-smallest-to-largest = De menor a mayor\nsort-largest-to-smallest = De mayor a menor\nremoving-from-recents =\n    Quitando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de { recents }\nremoved-from-recents =\n    { $items ->\n        [one] Se ha quitado elemento\n       *[other] Se han quitado { $items } elementos\n    } de { recents }\nsetting-permissions = Estableciendo permisos para \"{ $name } \" a { $mode }\nprogress-failed = { $percent } %, con errores\nset-permissions = Establecer permisos de \"{ $name }\" como { $mode }\n"
  },
  {
    "path": "i18n/es-419/cosmic_files.ftl",
    "content": "cosmic-files = Archivos de COSMIC\nempty-folder = Carpeta vacía\nempty-folder-hidden = Carpeta vacía (tiene elementos ocultos)\nno-results = No se encontraron resultados\nfilesystem = Sistema de archivos\nhome = Inicio\nnetworks = Redes\nnotification-in-progress = Las operaciones de archivo están en progreso\ntrash = Papelera\nrecents = Recientes\nundo = Deshacer\ntoday = Hoy\n# Desktop view options\ndesktop-view-options = Opciones de vista del escritorio...\nshow-on-desktop = Mostrar en el escritorio\ndesktop-folder-content = Contenido de la carpeta del escritorio\nmounted-drives = Unidades montadas\ntrash-folder-icon = Icono de la papelera\nicon-size-and-spacing = Tamaño y espaciado de los iconos\nicon-size = Tamaño del icono\n# List view\nname = Nombre\nmodified = Modificado\ntrashed-on = Enviado a la papelera\nsize = Tamaño\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Crear archivo comprimido\n\n## Empty Trash Dialog\n\nempty-trash = Vaciar la papelera\nempty-trash-warning = ¿Estás seguro de que deseas eliminar permanentemente todos los elementos de la papelera?\n\n## New File/Folder Dialog\n\ncreate-new-file = Crear nuevo archivo\ncreate-new-folder = Crear nueva carpeta\nfile-name = Nombre de archivo\nfolder-name = Nombre de carpeta\nfile-already-exists = Ya existe un archivo con ese nombre.\nfolder-already-exists = Ya existe una carpeta con ese nombre.\nname-hidden = Los nombres que comiencen con \".\" se ocultarán.\nname-invalid = El nombre no puede ser \"{ $filename }\".\nname-no-slashes = El nombre no puede contener barras.\n\n## Open/Save Dialog\n\ncancel = Cancelar\ncreate = Crear\nopen = Abrir\nopen-file = Abrir archivo\nopen-folder = Abrir carpeta\nopen-in-new-tab = Abrir en una nueva pestaña\nopen-in-new-window = Abrir en una nueva ventana\nopen-item-location = Abrir ubicación del elemento\nopen-multiple-files = Abrir múltiples archivos\nopen-multiple-folders = Abrir múltiples carpetas\nsave = Guardar\nsave-file = Guardar archivo\n\n## Open With Dialog\n\nopen-with-title = ¿Cómo deseas abrir \"{ $name }\"?\nbrowse-store = Explorar { $store }\n\n## Rename Dialog\n\nrename-file = Cambiar el nombre del archivo\nrename-folder = Cambiar el nombre de la carpeta\n\n## Replace Dialog\n\nreplace = Reemplazar\nreplace-title = \"{ $filename }\" ya existe en esta ubicación.\nreplace-warning = ¿Quieres reemplazarlo con el que estás guardando? Reemplazarlo sobrescribirá su contenido.\nreplace-warning-operation = ¿Quieres reemplazarlo? Reemplazarlo sobrescribirá su contenido.\noriginal-file = Archivo original\nreplace-with = Reemplazar con\napply-to-all = Aplicar a todos\nkeep-both = Conservar ambos\nskip = Saltar\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Establecer como ejecutable y ejecutar\nset-executable-and-launch-description = ¿Deseas establecer \"{ $name }\" como ejecutable y abrirlo?\nset-and-launch = Establecer y ejecutar\n\n## Metadata Dialog\n\nowner = Propietario\ngroup = Grupo\nother = Otro\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Agregar unidad de red\nconnect = Conectar\nconnect-anonymously = Conectar de forma anónima\nconnecting = Conectando...\ndomain = Dominio\nenter-server-address = Ingresa la dirección del servidor\nnetwork-drive-description =\n    Las direcciones de los servidores incluyen un prefijo de protocolo y una dirección.\n    Ejemplos: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocolos disponibles,Prefijo\n    AppleTalk,afp://\n    Protocolo de transferencia de archivos,ftp:// o ftps://\n    Sistema de archivos de red,nfs://\n    Bloque de mensajes del servidor,smb://\n    Protocolo de transferencia de archivos SSH,sftp:// o ssh://\n    WebDav,dav:// o davs://\nnetwork-drive-error = No se puede acceder a la unidad de red\npassword = Contraseña\nremember-password = Recordar contraseña\ntry-again = Intentar de nuevo\nusername = Nombre de usuario\n\n## Operations\n\nedit-history = Historial de ediciones\nhistory = Historial\nno-history = No hay elementos en el historial.\npending = Pendientes\nfailed = Con error\ncomplete = Completadas\ncompressing =\n    Comprimiendo { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items ->\n        [one] Se ha comprimido un elemento\n       *[other] Se han comprimidos { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\ncopy_noun = Copiar\ncreating = Creando \"{ $name }\" en \"{ $parent }\"\ncreated = Se ha creado \"{ $name }\" en \"{ $parent }\"\ncopying =\n    Copiando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncopied =\n    { $items ->\n        [one] Se ha copiado un elemento\n       *[other] Se han copiado { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\nemptying-trash = Vaciando la { trash } ({ $progress })...\nemptied-trash = Se ha vaciado la { trash }\nextracting =\n    Extrayendo { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nextracted =\n    { $items ->\n        [one] Se ha extraído un elemento\n       *[other] Se han extraído { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\nsetting-executable-and-launching = Estableciendo \"{ $name }\" como ejecutable y abriendo\nset-executable-and-launched = Se ha establecido \"{ $name }\" como ejecutable y se ha abierto\nmoving =\n    Moviendo { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de \"{ $from }\" a \"{ $to }\" ({ $progress })...\nmoved =\n    { $items ->\n        [one] Se ha movido un elemento\n       *[other] Se han movido { $items } elementos\n    } de \"{ $from }\" a \"{ $to }\"\nrenaming = Cambiando el nombre de \"{ $from }\" a \"{ $to }\"\nrenamed = Se ha cambiado el nombre de \"{ $from }\" a \"{ $to }\"\nrestoring =\n    Restaurando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de la { trash } ({ $progress })...\nrestored =\n    { $items ->\n        [one] Se ha restaurado un elemento\n       *[other] Se han restaurado { $items } elementos\n    } de la { trash }\nunknown-folder = carpeta desconocida\n\n## Open with\n\nmenu-open-with = Abrir con...\ndefault-app = { $name } (predeterminado)\n\n## Show details\n\nshow-details = Mostrar detalles\n\n## Settings\n\nsettings = Configuración\n\n### Appearance\n\nappearance = Apariencia\ntheme = Tema\nmatch-desktop = Igual que el escritorio\ndark = Oscuro\nlight = Claro\n# Context menu\nadd-to-sidebar = Añadir a la barra lateral\ncompress = Comprimir\nextract-here = Extraer aquí\nnew-file = Archivo nuevo...\nnew-folder = Carpeta nueva...\nopen-in-terminal = Abrir en una terminal\nmove-to-trash = Mover a la papelera\nrestore-from-trash = Restaurar de la papelera\nremove-from-sidebar = Quitar de la barra lateral\nsort-by-name = Ordenar por nombre\nsort-by-modified = Ordenar por modificado\nsort-by-size = Ordenar por tamaño\nsort-by-trashed = Ordenar por fecha de eliminación\n\n## Desktop\n\nchange-wallpaper = Cambiar fondo de pantalla...\ndesktop-appearance = Apariencia del escritorio...\ndisplay-settings = Configuración de pantalla...\n\n# Menu\n\n\n## File\n\nfile = Archivo\nnew-tab = Nueva pestaña\nnew-window = Nueva ventana\nrename = Cambiar nombre...\nclose-tab = Cerrar pestaña\nquit = Cerrar\n\n## Edit\n\nedit = Editar\ncut = Cortar\ncopy = Copiar\npaste = Pegar\nselect-all = Seleccionar todo\n\n## View\n\nzoom-in = Ampliar\ndefault-size = Tamaño predeterminado\nzoom-out = Disminuir\nview = Vistar\ngrid-view = Vista de cuadrícula\nlist-view = Vista de lista\nshow-hidden-files = Mostrar archivos ocultos\nlist-directories-first = Mostrar directorios primero\ngallery-preview = Vista previa de la galería\nmenu-settings = Configuración...\nmenu-about = Acerca de archivos COSMIC...\n\n## Sort\n\nsort = Ordenar\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Más reciente primero\nsort-oldest-first = Más antiguo primero\nsort-smallest-to-largest = De menor a mayor\nsort-largest-to-smallest = De mayor a menor\nrepository = Repositorio\nsupport = Apoyo\ndetails = Detalles\ndismiss = Descartar mensaje\nremove = Eliminar\ncancelled = Canceladas\ngrid-spacing = Espaciado de cuadrícula\noperations-running =\n    { $running ->\n        [one] Operación de { $running }\n       *[other] Operaciones de { $running }\n    } en ejecución ({ $percent } %)...\noperations-running-finished =\n    { $running ->\n        [one] Operación de { $running }\n       *[other] Operaciones de { $running }\n    } en ejecución ({ $percent } %), { $finished } completada(s)...\npause = Pausar\nresume = Reanudar\nextract-password-required = Contraseña requerida\nextract-to = Extraer en...\nextract-to-title = Extraer a una carpeta\nmount-error = No se puede acceder a la unidad\nother-apps = Otras aplicaciones\nrelated-apps = Aplicaciones relacionadas\nselected-items = los { $items } archivos seleccionados\npermanently-delete-question = Eliminar de forma permanente\ndelete = Eliminar\npermanently-delete-warning = ¿Estás seguro de que quieres eliminar { $target } de forma permanente? Esta acción no se puede deshacer.\nopen-with = Abrir con\nnone = Ninguno\nexecute-only = Solo ejecución\nwrite-only = Solo escritura\nwrite-execute = Escritura y ejecución\nread-only = Solo lectura\nread-execute = Lectura y ejecución\nread-write = Lectura y escritura\nread-write-execute = Lectura, escritura y ejecución\nfavorite-path-error = Error al abrir el directorio\nfavorite-path-error-description =\n    No se puede abrir \"{ $path }\".\n    Puede que no exista o que no tengas permiso a abrirlo.\n\n    ¿Quieres eliminarlo de la barra lateral?\nkeep = Reservar\nprogress = { $percent } %\nprogress-cancelled = { $percent } %, cancelado\nprogress-failed = { $percent } %, con errores\nprogress-paused = { $percent } %, pausado\nsetting-permissions = Estableciendo permisos de \"{ $name }\" como { $mode }\nset-permissions = Establecer permisos de \"{ $name }\" como { $mode }\npermanently-deleting =\n    Eliminando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de forma permanente\ndeleting =\n    Eliminando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de la { trash } ({ $progress })...\ndeleted =\n    { $items ->\n        [one] Se ha eliminado un elemento\n       *[other] Se han eliminado { $items } elementos\n    } de la { trash }\npermanently-deleted =\n    { $items ->\n        [one] Se ha eliminado un elemento\n       *[other] Se han eliminado { $items } elementos\n    } de forma permanente\nremoving-from-recents =\n    Quitando { $items } { $items ->\n        [one] elemento\n       *[other] elementos\n    } de { recents }\nremoved-from-recents =\n    { $items ->\n        [one] Se ha quitado elemento\n       *[other] Se han quitado { $items } elementos\n    } de { recents }\nreload-folder = Recargar la carpeta\nremove-from-recents = Quitar de recientes\ncalculating = Calculando...\ntype = Tipo: { $mime }\nitems = Elementos: { $items }\nitem-size = Tamaños: { $size }\nitem-created = Creado: { $created }\nitem-modified = Modificado: { $modified }\nitem-accessed = Accedido: { $accessed }\nsingle-click = Abrir con un solo clic\ntype-to-search = Escribir para buscar\ntype-to-search-recursive = Buscar en la carpeta actual y todas las subcarpetas\ntype-to-search-enter-path = Introducir la ruta al directorio o archivo\ndelete-permanently = Eliminar de forma permanente\neject = Expulsar\n"
  },
  {
    "path": "i18n/es-MX/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/et/cosmic_files.ftl",
    "content": "empty-folder = Tühi kaust\nempty-folder-hidden = Tühi kaust (leidub peidetud kirjeid)\nno-results = Tulemusi ei leidu\nfilesystem = Failisüsteem\nhome = Avaleht\nnetworks = Võrguühendused\nopen-file = Ava fail\nappearance = Välimus\ntheme = Kujundus\nmatch-desktop = Sobita töölauaga\ndark = Tume kujundus\nlight = Hele kujundus\nopen-folder = Ava kaust\nsettings = Seadistused\nfile = Fail\nquit = Välju\nconnect = Ühenda\nsupport = Kasutajatugi\ndelete = Kustuta\nname = Nimi\nnew-tab = Uus vahekaart\nnew-window = Uus aken\nclose-tab = Sulge vahekaart\nedit = Muuda\ncopy = Kopeeri\npaste = Aseta\nselect-all = Vali kõik\ncancel = Katkesta\nopen = Ava\nremove = Eemalda\ncreate = Loo\nrepository = Tarkvarahoidla\nreplace = Asenda\nsave = Salvesta\nundo = Võta tegevus tagasi\npassword = Salasõna\ncut = Lõika\nusername = Kasutajanimi\nview = Vaata\ntrash = Prügikast\nrecents = Viimati kasutatud\ntoday = Täna\nmenu-settings = Seadistused...\nmodified = Muudetud\ntrashed-on = Visatud prügikasti\nsize = Suurus\ndetails = Üksikasjad\npause = Peata\nresume = Jätka\nskip = Jäta vahele\nzoom-in = Suumi sisse\ndefault-size = Tavasuurus\nzoom-out = Suumi välja\nsave-file = Salvesta fail\ncreate-new-folder = Lisa uus kaust\ncreate-new-file = Lisa uus fail\nfile-name = Failinimi\nfolder-name = Kausta nimi\nfile-already-exists = Sellise nimega fail on juba olemas.\nfolder-already-exists = Sellise nimega kaust on juba olemas.\nname-no-slashes = Nimes ei tohi olla kaldkriipse.\nshow-on-desktop = Näita töölaual\ndesktop-folder-content = Töölauakausta sisu\nmounted-drives = Haagitud andmekandjad\ntrash-folder-icon = Prügikastikausta ikoon\nicon-size-and-spacing = Ikooni suurus ja vahed\nicon-size = Ikooni suurus\ngrid-spacing = Vahed ruudustikus\noperations-running =\n    { $running } { $running ->\n        [one] tegevus\n       *[other] tegevust\n    } on töös ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] tegevus\n       *[other] tegevust\n    } on töös ({ $percent }%), { $finished } lõppenud...\nname-invalid = Nimi ei saa olla „{ $filename }“.\nname-hidden = Kui nime alguses on punkt, siis fail või kaust muutub peidetuks.\nopen-in-new-tab = Ava uuel vahekaardil\nopen-in-new-window = Ava uues aknas\nopen-item-location = Ava objekti asukoht\nopen-multiple-files = Ava mitu faili\nopen-multiple-folders = Ava mitu kausta\ncancelled = Katkestatud\npending = Ootel\nfailed = Ebaõnnestunud\ncomplete = Tehtud\ndismiss = Suulge sõnum\nnotification-in-progress = Tegevused failidega on pooleli.\ndesktop-view-options = Töölauavaate valikud...\ncreate-archive = Loo arhiivifail\nextract-password-required = Salasõna on vajalik\nextract-to = Paki lahti siia...\nextract-to-title = Paku lahti kausta\nempty-trash = Tühjenda prügikast\nempty-trash-warning = Kas oled kindel, et soovid jäädavalt kustutada prügikasti sisu?\nrelated-apps = Seotud rakendused\nrename-file = Muuda faili nime\nrename-folder = Muuda kausta nime\nreplace-title = „{ $filename }“ fail juba on selles asukohas olemas.\nopen-with-title = Kuidas sa sooviksid avada „{ $name }“ faili?\nbrowse-store = Vaata siia: { $store }\nother-apps = Muud rakendused\nselected-items = { $items } valitud objekt(i)\npermanently-delete-question = Kas kustutame jäädavalt?\npermanently-delete-warning = Kas sa oled kindel, et soovid jäädavalt kustutada: { $target }? Seda tegevust ei saa tagasi pöörata.\nfavorite-path-error = Viga kausta avamisel\nadd-network-drive = Lisa võrguketas\nconnect-anonymously = Ühenda anonüümselt\nconnecting = Ühendamisel...\ndomain = Domeen\nenter-server-address = Sisesta serveri aadress\nremember-password = Jäta salasõna meelde\ntry-again = Proovi uuesti\nedit-history = Muutmiste ajalugu\nhistory = Ajalugu\nno-history = Ajaloos pole ühtegi kirjet.\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, katkestatud\nprogress-failed = { $percent }%, ebaõnnestus\nprogress-paused = { $percent }%, peatatud\nunknown-folder = tundmatu kaust\nmenu-open-with = Ava rakendusega…\ndefault-app = { $name } (vaikimisi)\nshow-details = Näita üksikasju\ntype = Tüüp: { $mime }\nitems = Objekte: { $items }\nitem-size = Suurus: { $size }\nitem-created = Loodud: { $created }\nitem-modified = Muudetud: { $modified }\nitem-accessed = Kasutatud: { $accessed }\ncalculating = Arvutan...\nsingle-click = Ava ühe klõpsuga\nreload-folder = Laadi kaust uuesti\nrename = Muuda nime...\ngrid-view = Ruudustikuvaade\nlist-view = Loendivaade\nshow-hidden-files = Näita peidetud faile\nlist-directories-first = Lisa kaustad loendi algusesse\ngallery-preview = Galerii eelvaade\ncosmic-files = COSMICu failid\nmount-error = Ligipääs andmekandjale puudub\nreplace-warning = Kas sa soovid ta asendada sellega, mida oled salvestamas? Samaga asendub ka kogu sisu.\nreplace-warning-operation = Kas sa soovid ta asendada? Samaga asendub ka kogu sisu.\noriginal-file = Algfail\nreplace-with = Asenda järgnevaga\napply-to-all = Kohalda kõigile\nkeep-both = Jäta mõlemad alles\nset-executable-and-launch = Märgi käivitatavaks ja käivita\nset-executable-and-launch-description = Kas sa soovid „{ $name }“ faili märkida käivitatavaks ja ta käivitada?\nset-and-launch = Märgi ja käivita\nopen-with = Ava rakendusega\nowner = Omanik\ngroup = Grupp\nother = Teised\nnone = Määramata\nexecute-only = Ainult käivitatav\nwrite-only = Ainult kirjutatav\nwrite-execute = Kirjutatav ja käivitatav\nread-only = Ainult loetav\nread-execute = Loetav ja käivitatav\nread-write = Loetav ja kirjutatav\nread-write-execute = Loetav, kirjutatav ja käivitatav\nfavorite-path-error-description =\n    „{ $path }“ asukoha avamine ei õnnestu.\n    Teda kas pole olemas või sul pole õigusi tema avamiseks.\n\n    Kas sooviksid ta külgribalt eemaldada?\nkeep = Säilita\nnetwork-drive-description =\n    Serveri aadressides peab olema protokolli eesliide ja aadress ise.\n    Näited: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Kasutatavad protokollid,eesliide\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// või ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// või ssh://\n    WebDAV,dav:// või davs://\nnetwork-drive-error = Puudub ligipääs võrgus asuvale andmekandjale\ntype-to-search = Otsimiseks kirjuta\ntype-to-search-recursive = Otsing sellest kaustast ja alamkaustadest\ntype-to-search-enter-path = Sisestab kausta või faili asukoha\nadd-to-sidebar = Lisa külgribale\nremove-from-sidebar = Eemalda külgribalt\ncopy_noun = Kopeeri\ncreating = Loon: „{ $name }“ asukohas „{ $parent }“\ncreated = „{ $name }“ on loodud asukohta „{ $parent }“\ncompress = Paki kokku\ndelete-permanently = Kustuta jäädavalt\neject = Väljasta\nextract-here = Paki lahti\nnew-file = Uus fail...\nnew-folder = Uus kaust...\nopen-in-terminal = Ava terminalis\nmove-to-trash = Viska prügikasti\nrestore-from-trash = Taasta prügikastist\nsort-by-name = Järjesta nime alusel\nsort-by-modified = Järjesta muutmise alusel\nsort-by-size = Järjesta suuruse alusel\nsort-by-trashed = Järjesta kustutamise aja alusel\nremove-from-recents = Eemalda hiljutiste failide loendist\nchange-wallpaper = Muuda taustapilti...\ndesktop-appearance = Töölaua välimus...\ndisplay-settings = Ekraani seadistused...\nmenu-about = Rakenduse teave: COSMICu failid...\nsort = Järjesta\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Esmalt uuemad\nsort-oldest-first = Esmalt vanemad\nsort-smallest-to-largest = Väiksemast suuremani\nsort-largest-to-smallest = Suuremast väiksemani\n"
  },
  {
    "path": "i18n/eu/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/fa/cosmic_files.ftl",
    "content": "cosmic-files = فایل‌های COSMIC\nempty-folder = پوشه خالی\nempty-folder-hidden = پوشه خالی (شامل موارد مخفی)\nno-results = نتیجه‌ای یافت نشد\nfilesystem = فایل سیستم\nhome = خانه\nnetworks = شبکه‌ها\nnotification-in-progress = عملیات فایل در حال انجام است.\ntrash = زباله‌دان\nrecents = موارد اخیر\nundo = بازگردانی\ntoday = امروز\n# Desktop view options\ndesktop-view-options = گزینه‌های نمایش دسکتاپ...\nshow-on-desktop = نمایش در دسکتاپ\ndesktop-folder-content = محتوای پوشه دسکتاپ\nmounted-drives = درایوهای متصل‌شده\ntrash-folder-icon = آیکون پوشه زباله‌دان\nicon-size-and-spacing = اندازه و فاصله آیکون‌ها\nicon-size = اندازه آیکون\ngrid-spacing = فاصله شبکه‌ای\n# List view\nname = نام\nmodified = زمان تغییر\ntrashed-on = زمان حذف\nsize = حجم\n# Progress footer\ndetails = جزئیات\ndismiss = بستن پیام\noperations-running = { $running } عملیات در حال اجرا ({ $percent }%)...\noperations-running-finished = { $running } عملیات در حال اجرا ({ $percent }%)، { $finished } پایان یافته...\npause = توقف\nresume = ادامه\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = ایجاد بایگانی\n\n## Extract Dialog\n\nextract-password-required = رمز عبور مورد نیاز است\nextract-to = استخراج در...\nextract-to-title = استخراج در پوشه\n\n## Empty Trash Dialog\n\nempty-trash = خالی کردن زباله‌دان\nempty-trash-warning = آیا از حذف دائمی همه موارد در زباله‌دان مطمئن هستید؟\n\n## Mount Error Dialog\n\nmount-error = دسترسی به درایو ممکن نیست\n\n## New File/Folder Dialog\n\ncreate-new-file = ایجاد فایل جدید\ncreate-new-folder = ایجاد پوشه جدید\nfile-name = نام فایل\nfolder-name = نام پوشه\nfile-already-exists = فایلی با این نام وجود دارد.\nfolder-already-exists = پوشه‌ای با این نام وجود دارد.\nname-hidden = نام‌هایی که با \".\" شروع شوند مخفی خواهند بود.\nname-invalid = نام نمی‌تواند \"{ $filename }\" باشد.\nname-no-slashes = نام نمی‌تواند حاوی اسلش باشد.\n\n## Open/Save Dialog\n\ncancel = لغو\ncreate = ایجاد\nopen = باز کردن\nopen-file = باز کردن فایل\nopen-folder = باز کردن پوشه\nopen-in-new-tab = باز کردن در زبانه جدید\nopen-in-new-window = باز کردن در پنجره جدید\nopen-item-location = باز کردن محل مورد\nopen-multiple-files = باز کردن چندین فایل\nopen-multiple-folders = باز کردن چندین پوشه\nsave = ذخیره\nsave-file = ذخیره فایل\n\n## Open With Dialog\n\nopen-with-title = چگونه می‌خواهید \"{ $name }\" را باز کنید؟\nbrowse-store = مرور { $store }\nother-apps = برنامه‌های دیگر\nrelated-apps = برنامه‌های مرتبط\n\n## Permanently delete Dialog\n\nselected-items = { $items } مورد انتخاب شده\npermanently-delete-question = حذف دائمی\ndelete = حذف\npermanently-delete-warning = آیا از حذف دائمی { $target } مطمئن هستید؟ این عمل قابل بازگشت نیست.\n\n## Rename Dialog\n\nrename-file = تغییر نام فایل\nrename-folder = تغییر نام پوشه\n\n## Replace Dialog\n\nreplace = جایگزین کردن\nreplace-title = \"{ $filename }\" از قبل در این مکان وجود دارد.\nreplace-warning = آیا می‌خواهید آن را با موردی که در حال ذخیره هستید جایگزین کنید؟ این عمل محتوای آن را بازنویسی خواهد کرد.\nreplace-warning-operation = آیا می‌خواهید آن را جایگزین کنید؟ این عمل محتوای آن را بازنویسی خواهد کرد.\noriginal-file = فایل اصلی\nreplace-with = جایگزینی با\napply-to-all = اعمال برای همه\nkeep-both = نگه‌داشتن هر دو\nskip = رد کردن\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = تنظیم به عنوان فایل اجرایی و اجرا\nset-executable-and-launch-description = آیا می‌خواهید \"{ $name }\" را به عنوان فایل اجرایی تنظیم کرده و اجرا کنید؟\nset-and-launch = تنظیم و اجرا\n\n## Metadata Dialog\n\nopen-with = باز کردن با\nowner = مالک\ngroup = گروه\nother = سایر\n\n### Mode 0\n\nnone = هیچ‌کدام\n\n### Mode 1 (unusual)\n\nexecute-only = فقط اجرا\n\n### Mode 2 (unusual)\n\nwrite-only = فقط نوشتن\n\n### Mode 3 (unusual)\n\nwrite-execute = نوشتن و اجرا\n\n### Mode 4\n\nread-only = فقط خواندن\n\n### Mode 5\n\nread-execute = خواندن و اجرا\n\n### Mode 6\n\nread-write = خواندن و نوشتن\n\n### Mode 7\n\nread-write-execute = خواندن، نوشتن و اجرا\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = خطا در باز کردن مسیر\nfavorite-path-error-description =\n    باز کردن \"{ $path }\" امکان‌پذیر نیست.\n    ممکن است وجود نداشته باشد یا شما اجازه باز کردن آن را نداشته باشید.\n\n    آیا می‌خواهید آن را از نوار کناری حذف کنید؟\nremove = حذف\nkeep = نگه داشتن\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = افزودن درایو شبکه\nconnect = اتصال\nconnect-anonymously = اتصال ناشناس\nconnecting = در حال اتصال...\ndomain = دامنه\nenter-server-address = آدرس سرور را وارد کنید\nnetwork-drive-description =\n    آدرس‌های سرور شامل پیشوند پروتکل و آدرس هستند.\n    نمونه: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    پروتکل‌های موجود,پیشوند\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// یا ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// یا ssh://\n    WebDav,dav:// یا davs://\nnetwork-drive-error = دسترسی به درایو شبکه ممکن نیست\npassword = رمز عبور\nremember-password = به خاطر سپردن رمز عبور\ntry-again = تلاش دوباره\nusername = نام کاربری\n\n## Operations\n\ncancelled = لغو شد\nedit-history = ویرایش تاریخچه\nhistory = تاریخچه\nno-history = هیچ موردی در تاریخچه وجود ندارد.\npending = در حال انتظار\nprogress = { $percent }%\nprogress-cancelled = { $percent }%، لغو شد\nprogress-paused = { $percent }%، متوقف شد\nfailed = ناموفق\ncomplete = کامل شد\ncompressing = در حال فشرده‌سازی { $items } مورد از \"{ $from }\" به \"{ $to }\" ({ $progress })...\ncompressed = { $items } مورد از \"{ $from }\" به \"{ $to }\" فشرده شد\ncopy_noun = کپی\ncreating = در حال ایجاد \"{ $name }\" در \"{ $parent }\"\ncreated = \"{ $name }\" در \"{ $parent }\" ایجاد شد\ncopying = در حال کپی { $items } مورد از \"{ $from }\" به \"{ $to }\" ({ $progress })...\ncopied = { $items } مورد از \"{ $from }\" به \"{ $to }\" کپی شد\ndeleting = در حال حذف { $items } مورد از { trash } ({ $progress })...\ndeleted = { $items } مورد از { trash } حذف شد\nemptying-trash = در حال خالی کردن { trash } ({ $progress })...\nemptied-trash = { trash } خالی شد\nextracting = در حال استخراج { $items } مورد از \"{ $from }\" به \"{ $to }\" ({ $progress })...\nextracted = { $items } مورد از \"{ $from }\" به \"{ $to }\" استخراج شد\nsetting-executable-and-launching = در حال تنظیم \"{ $name }\" به عنوان فایل اجرایی و اجرا\nset-executable-and-launched = \"{ $name }\" به عنوان فایل اجرایی تنظیم و اجرا شد\nsetting-permissions = در حال تنظیم مجوزهای \"{ $name }\" به { $mode }\nset-permissions = مجوزهای \"{ $name }\" به { $mode } تنظیم شد\nmoving = در حال انتقال { $items } مورد از \"{ $from }\" به \"{ $to }\" ({ $progress })...\nmoved = { $items } مورد از \"{ $from }\" به \"{ $to }\" منتقل شد\npermanently-deleting = در حال حذف دائمی { $items } مورد\npermanently-deleted = { $items } مورد به صورت دائمی حذف شد\nremoving-from-recents = در حال حذف { $items } مورد از { recents }\nremoved-from-recents = { $items } مورد از { recents } حذف شد\nrenaming = تغییر نام \"{ $from }\" به \"{ $to }\"\nrenamed = \"{ $from }\" به \"{ $to }\" تغییر نام یافت\nrestoring = در حال بازیابی { $items } مورد از { trash } ({ $progress })...\nrestored = { $items } مورد از { trash } بازیابی شد\nunknown-folder = پوشه ناشناس\n\n## Open with\n\nmenu-open-with = باز کردن با...\ndefault-app = { $name } (پیش‌فرض)\n\n## Show details\n\nshow-details = نمایش جزئیات\ntype = نوع: { $mime }\nitems = مورد: { $items }\nitem-size = حجم: { $size }\nitem-created = ایجاد شده: { $created }\nitem-modified = تغییر یافته: { $modified }\nitem-accessed = دسترسی یافته: { $accessed }\ncalculating = در حال محاسبه...\n\n## Settings\n\nsettings = تنظیمات\nsingle-click = باز کردن با یک کلیک\n\n### Appearance\n\nappearance = ظاهر\ntheme = پوسته\nmatch-desktop = مطابق با دسکتاپ\ndark = تاریک\nlight = روشن\n\n### Type to Search\n\ntype-to-search = جستجو با تایپ کردن\ntype-to-search-recursive = جستجو در پوشه فعلی و تمام زیرپوشه‌ها\ntype-to-search-enter-path = وارد کردن مسیر پوشه یا فایل\n# Context menu\nadd-to-sidebar = افزودن به نوار کناری\ncompress = فشرده‌سازی\ndelete-permanently = حذف دائمی\neject = خارج کردن\nextract-here = استخراج\nnew-file = فایل جدید...\nnew-folder = پوشه جدید...\nopen-in-terminal = باز کردن در ترمینال\nmove-to-trash = انتقال به زباله‌دان\nrestore-from-trash = بازیابی از زباله‌دان\nremove-from-sidebar = حذف از نوار کناری\nsort-by-name = مرتب‌سازی بر اساس نام\nsort-by-modified = مرتب‌سازی بر اساس زمان تغییر\nsort-by-size = مرتب‌سازی بر اساس حجم\nsort-by-trashed = مرتب‌سازی بر اساس زمان حذف\nremove-from-recents = حذف از موارد اخیر\n\n## Desktop\n\nchange-wallpaper = تغییر تصویر پس‌زمینه...\ndesktop-appearance = ظاهر دسکتاپ...\ndisplay-settings = تنظیمات نمایش...\n\n# Menu\n\n\n## File\n\nfile = فایل\nnew-tab = زبانه جدید\nnew-window = پنجره جدید\nreload-folder = بازخوانی پوشه\nrename = تغییر نام...\nclose-tab = بستن زبانه\nquit = خروج\n\n## Edit\n\nedit = ویرایش\ncut = برش\ncopy = کپی\npaste = جایگذاری\nselect-all = انتخاب همه\n\n## View\n\nzoom-in = بزرگ‌نمایی\ndefault-size = اندازه پیش‌فرض\nzoom-out = کوچک‌نمایی\nview = نمایش\ngrid-view = نمایش شبکه‌ای\nlist-view = نمایش فهرستی\nshow-hidden-files = نمایش فایل‌های مخفی\nlist-directories-first = نمایش پوشه‌ها در ابتدا\ngallery-preview = پیش‌نمایش گالری\nmenu-settings = تنظیمات...\nmenu-about = درباره فایل‌های COSMIC...\n\n## Sort\n\nsort = مرتب‌سازی\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = جدیدترین در ابتدا\nsort-oldest-first = قدیمی‌ترین در ابتدا\nsort-smallest-to-largest = کوچک‌ترین به بزرگ‌ترین\nsort-largest-to-smallest = بزرگ‌ترین به کوچک‌ترین\n"
  },
  {
    "path": "i18n/fi/cosmic_files.ftl",
    "content": "cosmic-files = COSMICin tiedostot\nempty-folder = Tyhjä kansio\nempty-folder-hidden = Tyhjä kansio (sisältää piilotettuja kohteita)\nno-results = Ei tuloksia\nfilesystem = Tiedostojärjestelmä\nhome = Koti\nnetworks = Verkot\nnotification-in-progress = Tiedostotoimintoja käynnissä\ntrash = Roskakori\nrecents = Viimeaikaiset\nundo = Kumoa\ntoday = Tänään\n\n# Desktop view options\n\ndesktop-view-options = Työpöytänäkymän asetukset…\nshow-on-desktop = Näytä työpöydällä\ndesktop-folder-content = Työpöytäkansion sisältö\nmounted-drives = Liitetyt asemat\ntrash-folder-icon = Roskakorikansion kuvake\nicon-size-and-spacing = Kuvakkeen koko ja välistys\nicon-size = Kuvakkeen koko\n\n# List view\n\nname = Nimi\nmodified = Muokattu\ntrashed-on = Siirretty roskakoriin\nsize = Koko\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Luo arkisto\n\n## Empty Trash Dialog\n\nempty-trash = Tyhjennä roskakori\nempty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi\n\n## Mount Error Dialog\n\nmount-error = Levy on saavuttamattomissa\n\n## New File/Folder Dialog\n\ncreate-new-file = Luo uusi tiedosto\ncreate-new-folder = Luo uusi kansio\nfile-name = Tiedoston nimi\nfolder-name = Kansion nimi\nfile-already-exists = Tiedosto samalla nimellä on jo olemassa\nfolder-already-exists = Kansio samalla nimellä on jo olemassa\nname-hidden = Merkillä \".\" alkavat nimet piilotetaan\nname-invalid = Nimi ei voi olla \"{ $filename }\"\nname-no-slashes = Nimi ei voi sisältää vinoviivoja\n\n## Open/Save Dialog\n\ncancel = Peru\ncreate = Luo\nopen = Avaa\nopen-file = Avaa tiedosto\nopen-folder = Avaa kansio\nopen-in-new-tab = Avaa uudessa välilehdessä\nopen-in-new-window = Avaa uudessa ikkunassa\nopen-item-location = Avaa kohteen sijainti\nopen-multiple-files = Avaa useita tiedostoja\nopen-multiple-folders = Avaa useita kansioita\nsave = Tallenna\nsave-file = Tallenna tiedosto\n\n## Open With Dialog\n\nopen-with-title = Miten haluat avata kohteen \"{ $name }\"?\nbrowse-store = Selaa { $store }a\n\n## Rename Dialog\n\nrename-file = Nimeä tiedosto uudelleen\nrename-folder = Nimeä kansio uudelleen\n\n## Replace Dialog\n\nreplace = Korvaa\nreplace-title = \"{ $filename }\" on jo olemassa tässä sijainnissa\nreplace-warning = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön.\nreplace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön.\noriginal-file = Alkuperäinen tiedosto\nreplace-with = Korvaa käyttäen\napply-to-all = Toteuta kaikkiin\nkeep-both = Pidä molemmat\nskip = Ohita\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Aseta käynnistettäväksi ja käynnistä\nset-executable-and-launch-description = Haluatko asettaa kohteen \"{ $name }\" käynnistettäväksi ja käynnistää sen?\nset-and-launch = Aseta ja käynnistä\n\n## Metadata Dialog\n\nowner = Omistaja\ngroup = Ryhmä\nother = Muut\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Lisää verkkolevy\nconnect = Yhdistä\nconnect-anonymously = Yhdistä nimettömästi\nconnecting = Yhdistetään…\ndomain = Verkkotunnus\nenter-server-address = Kirjoita palvelimen osoite\nnetwork-drive-description =\n    Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen.\n    Esimerkkejä: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Saatavilla olevat yhteyskäytännöt,Etuliite\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// tai ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// tai ssh://\n    WebDav,dav:// tai davs://\nnetwork-drive-error = Verkkolevy ei saatavilla\npassword = Salasana\nremember-password = Muista salasana\ntry-again = Yritä uudelleen\nusername = Käyttäjätunnus\n\n## Operations\n\nedit-history = Muokkaa historiaa\nhistory = Historia\nno-history = Historia on tyhjä.\npending = Jonossa\nfailed = Epäonnistuneet\ncomplete = Valmiit\ncompressing =\n    Pakataan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" arkistoon \"{ $to }\" ({ $progress })…\ncompressed =\n    Pakattu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" arkistoon \"{ $to }\"\ncopy_noun = Kopio\ncreating = Luodaan \"{ $name }\" kohteen \"{ $parent }\" alle\ncreated = Luotu \"{ $name }\" kohteen \"{ $parent }\" alle\ncopying =\n    Kopioidaan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" kohteeseen \"{ $to }\" ({ $progress })…\ncopied =\n    Kopioitu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" kohteeseen \"{ $to }\"\nemptying-trash = Tyhjennetään { trash } ({ $progress })…\nemptied-trash = Tyhjennetty { trash }\nextracting =\n    Puretaan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } arkistosta \"{ $from }\" kohteeseen \"{ $to }\" ({ $progress })…\nextracted =\n    Purettu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } arkistosta \"{ $from }\" kohteeseen \"{ $to }\"\nsetting-executable-and-launching = Asetetaan \"{ $name }\" käynnistettäväksi ja käynnistetään\nset-executable-and-launched = Asetettu \"{ $name }\" käynnistettäväksi ja käynnistetty\nmoving =\n    Siirretään { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" kohteeseen \"{ $to }\" ({ $progress })…\nmoved =\n    Siirretty { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } sijainnista \"{ $from }\" kohteeseen \"{ $to }\"\nrenaming = Nimetään kohde \"{ $from }\" muotoon \"{ $to }\"\nrenamed = Nimetty kohde \"{ $from }\" muotoon \"{ $to }\"\nrestoring =\n    Palautetaan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } roskakorista ({ $progress })…\nrestored =\n    Palautettu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } roskakorista\nunknown-folder = tuntematon kansio\n\n## Open with\n\nmenu-open-with = Avaa sovelluksella…\ndefault-app = { $name } (oletus)\n\n## Show details\n\nshow-details = Näytä yksityiskohdat\n\n## Settings\n\nsettings = Asetukset\n\n### Appearance\n\nappearance = Ulkoasu\ntheme = Teema\nmatch-desktop = Sovita työpöytään\ndark = Tumma\nlight = Vaalea\n\n# Context menu\n\nadd-to-sidebar = Lisää sivupalkkiin\ncompress = Pakkaa…\nextract-here = Pura\nnew-file = Uusi tiedosto…\nnew-folder = Uusi kansio…\nopen-in-terminal = Avaa päätteessä\nmove-to-trash = Siirrä roskakoriin\nrestore-from-trash = Palauta roskakorista\nremove-from-sidebar = Poista sivupalkista\nsort-by-name = Järjestä nimen mukaan\nsort-by-modified = Järjestä muokkausajan mukaan\nsort-by-size = Järjestä koon mukaan\nsort-by-trashed = Järjestä poistamisajan mukaan\n\n## Desktop\n\nchange-wallpaper = Vaihda taustakuvaa…\ndesktop-appearance = Työpöydän ulkoasu…\ndisplay-settings = Näytön asetukset…\n\n# Menu\n\n\n## File\n\nfile = Tiedosto\nnew-tab = Uusi välilehti\nnew-window = Uusi ikkuna\nrename = Nimeä uudelleen…\nclose-tab = Sulje välilehti\nquit = Lopeta\n\n## Edit\n\nedit = Muokkaa\ncut = Leikkaa\ncopy = Kopioi\npaste = Liitä\nselect-all = Valitse kaikki\n\n## View\n\nzoom-in = Lähennä\ndefault-size = Oletuskoko\nzoom-out = Loitonna\nview = Näytä\ngrid-view = Ruudukkonäkymä\nlist-view = Listanäkymä\nshow-hidden-files = Näytä piilotetut tiedostot\nlist-directories-first = Näytä kansiot ensin\ngallery-preview = Gallerian esikatselu\nmenu-settings = Asetukset…\nmenu-about = Tietoa COSMICin tiedostonhallinnasta…\n\n## Sort\n\nsort = Järjestä\nsort-a-z = A-Ö\nsort-z-a = Ö-A\nsort-newest-first = Uusin ensin\nsort-oldest-first = Vanhin ensin\nsort-smallest-to-largest = Pienimmästä suurimpaan\nsort-largest-to-smallest = Suurimmasta pienimpään\nresume = Jatka\nextract-password-required = Salasana vaaditaan\nextract-to-title = Pura kansioon\nempty-trash-title = Tyhjennetäänkö roskakori?\nother-apps = Muut sovellukset\nrelated-apps = Liittyvät sovellukset\npermanently-delete-question = Poistetaanko pysyvästi?\ndelete = Poista\nopen-with = Avaa sovelluksella\nremove = Poista\ncancelled = Peruttu\ntype = Tyyppi: { $mime }\nitem-size = Koko: { $size }\nitem-created = Luotu: { $created }\nitem-modified = Muokattu: { $modified }\ndelete-permanently = Poista pysyvästi\nreload-folder = Lataa kansio uudelleen\ncomment = Tiedostonhallinta COSMIC-työpöydälle\nkeywords = Folder;Manager;Kansio;Hakemisto;Hallinta;Hallinnointi;Hallitse;Hallinnoi;\ncopy-to-button-label = Kopioi\nmove-to-button-label = Siirrä\nclear-recents-history = Tyhjennä viimeaikaisten historia\ncopy-path = Kopioi polku\ndismiss = Hylkää viesti\noperations-running =\n    { $running } { $running ->\n        [one] toiminto\n       *[other] toimintoa\n    } käynnissä ({ $percent } %)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] toiminto\n       *[other] toimintoa\n    } käynnissä ({ $percent } %), { $finished } valmistunut…\npause = Keskeytä\nextract-to = Pura sijaintiin…\npermanently-delete-warning = { $target } tullaan poistamaan pysyvästi. Tätä toimintoa ei voi perua.\nexecute-only = Vain suoritus\nwrite-only = Vain kirjoitus\nwrite-execute = Kirjoita ja suorita\nread-only = Vain luku\nread-execute = Lue ja suorita\nread-write = Lue ja kirjoita\nread-write-execute = Lue, kirjoita sekä suorita\ncalculating = Lasketaan…\nsingle-click = Yhden napsautuksen avaus\ntype-to-search = Kirjoita etsiäksesi\ntype-to-search-recursive = Etsii nykyisestä kansiosta ja kaikista alikansioista\nremove-from-recents = Poista viimeaikaisista\nselected-items = { $items } valittua kohdetta\nshow-recents = Viimeaikaisten kansio sivupalkissa\ncopy-to = Kopioi…\nmove-to = Siirrä…\ndetails = Yksityiskohdat\ngrid-spacing = Ruudukkovälit\nnone = Ei mitään\nfavorite-path-error = Virhe avattaessa kansiota\nfavorite-path-error-description =\n    Polun { $path } avaaminen ei onnistunut\n    \"{ $path }\" ei välttämättä ole olemassa tai oikeutesi eivät riitä sen avaamiseen\n\n    Haluatko poistaa sen sivupalkista?\nkeep = Pidä\nrepository = Tietovarasto\nsupport = Tuki\nprogress = { $percent } %\nprogress-cancelled = { $percent } %, peruttu\nprogress-failed = { $percent } %, epäonnistui\nprogress-paused = { $percent } %, keskeytetty\nsetting-permissions = Asetetaan kohteen \"{ $name }\" käyttöoikeudeksi { $mode }\nset-permissions = Asetettu kohteen { $name } käyttöoikeudeksi { $mode }\npermanently-deleting =\n    Poistetaan pysyvästi { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    }\npermanently-deleted =\n    Poistettu pysyvästi { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    }\nitems = Kohteita: { $items }\nitem-accessed = Käytetty: { $accessed }\ntype-to-search-enter-path = Kirjoittaa polun kansioon tai tiedostoon\neject = Poista asemasta\ncopy-to-title = Valitse mihin kopioidaan\nmove-to-title = Valitse mihin siirretään\npasted-image = Liitetty kuva\npasted-text = Liitetty teksti\npasted-video = Liitetty video\ntype-to-search-select = Valitsee ensimmäisen täsmäävän tiedoston tai kansion\ndeleting =\n    Poistetaan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } roskakorista ({ $progress })…\ndeleted =\n    Poistettu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } roskakorista\nremoving-from-recents =\n    Poistetaan { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } viimeaikaisista\nremoved-from-recents =\n    Poistettu { $items } { $items ->\n        [one] kohde\n       *[other] kohdetta\n    } viimeaikaisista\n"
  },
  {
    "path": "i18n/fr/cosmic_files.ftl",
    "content": "cosmic-files = Fichiers COSMIC\nempty-folder = Dossier vide\nempty-folder-hidden = Dossier vide (contient des éléments cachés)\nno-results = Aucun résultat trouvé\nfilesystem = Système de fichiers\nhome = Dossier personnel\nnetworks = Réseaux\nnotification-in-progress = Des opérations sur des fichiers sont en cours\ntrash = Corbeille\nrecents = Récents\nundo = Annuler\ntoday = Aujourd'hui\n# Desktop view options\ndesktop-view-options = Options d'affichage du bureau...\nshow-on-desktop = Afficher sur le bureau\ndesktop-folder-content = Contenu du dossier du bureau\nmounted-drives = Lecteurs montés\ntrash-folder-icon = Icône du dossier Corbeille\nicon-size-and-spacing = Taille et espacement des icônes\nicon-size = Taille des icônes\ngrid-spacing = Espacement de la grille\n# List view\nname = Nom\nmodified = Modifié\ntrashed-on = Mis à la corbeille\nsize = Taille\n# Progress footer\ndetails = Détails\ndismiss = Ignorer le message\noperations-running =\n    { $running } { $running ->\n        [one] opération\n       *[other] opérations\n    } en cours ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] opération\n       *[other] opérations\n    } en cours ({ $percent }%), { $finished } terminé...\npause = Pause\nresume = Reprendre\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Créer une archive\n\n## Extract Dialog\n\nextract-password-required = Mot de passe requis\nextract-to = Extraire vers...\nextract-to-title = Extraire vers le dossier\n\n## Empty Trash Dialog\n\nempty-trash = Vider la corbeille\nempty-trash-warning = Les éléments de la corbeille seront définitivement supprimés\n\n## Mount Error Dialog\n\nmount-error = Impossible d'accéder au lecteur\n\n## New File/Folder Dialog\n\ncreate-new-file = Créer un nouveau fichier\ncreate-new-folder = Créer un nouveau dossier\nfile-name = Nom du fichier\nfolder-name = Nom du dossier\nfile-already-exists = Un fichier portant ce nom existe déjà\nfolder-already-exists = Un dossier portant ce nom existe déjà\nname-hidden = Les noms commençant par \".\" seront cachés\nname-invalid = Le nom ne peut pas être \"{ $filename }\"\nname-no-slashes = Le nom ne peut pas contenir de slashs\n\n## Open/Save Dialog\n\ncancel = Annuler\ncreate = Créer\nopen = Ouvrir\nopen-file = Ouvrir le fichier\nopen-folder = Ouvrir dossier\nopen-in-new-tab = Ouvrir dans un nouvel onglet\nopen-in-new-window = Ouvrir dans une nouvelle fenêtre\nopen-item-location = Ouvrir l'emplacement de l'élément\nopen-multiple-files = Ouvrir plusieurs fichiers\nopen-multiple-folders = Ouvrir plusieurs dossiers\nsave = Enregistrer\nsave-file = Enregistrer fichier\n\n## Open With Dialog\n\nopen-with-title = Comment souhaitez-vous ouvrir \"{ $name }\" ?\nbrowse-store = Parcourir { $store }\n\n## Permanently delete Dialog\n\nselected-items = Les { $items } éléments sélectionnés\npermanently-delete-question = Supprimer définitivement ?\ndelete = Supprimer\npermanently-delete-warning = { $target } sera définitivement supprimé. Cette action ne peut pas être annulée.\n\n## Rename Dialog\n\nrename-file = Renommer le fichier\nrename-folder = Renommer le dossier\n\n## Replace Dialog\n\nreplace = Remplacer\nreplace-title = \"{ $filename }\" existe déjà à cet endroit\nreplace-warning = Voulez-vous remplacer ce fichier par celui que vous enregistrez ? Cela écrasera son contenu.\nreplace-warning-operation = Voulez-vous remplacer ce fichier ? Cela écrasera son contenu.\noriginal-file = Fichier d'origine\nreplace-with = Remplacer par\napply-to-all = Appliquer à tous\nkeep-both = Conserver les deux\nskip = Ignorer\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Définir comme exécutable et lancer\nset-executable-and-launch-description = Voulez-vous définir \"{ $name }\" comme exécutable et le lancer ?\nset-and-launch = Définir et lancer\n\n## Metadata Dialog\n\nopen-with = Ouvrir avec\nowner = Propriétaire\ngroup = Groupe\nother = Autre\n\n### Mode 0\n\nnone = Aucun\n\n### Mode 1 (unusual)\n\nexecute-only = Exécution seule\n\n### Mode 2 (unusual)\n\nwrite-only = Écriture seule\n\n### Mode 3 (unusual)\n\nwrite-execute = Écriture et exécution\n\n### Mode 4\n\nread-only = Lecture seule\n\n### Mode 5\n\nread-execute = Lecture et exécution\n\n### Mode 6\n\nread-write = Lecture et écriture\n\n### Mode 7\n\nread-write-execute = Lecture, écriture et exécution\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Erreur lors de l'ouverture du répertoire\nfavorite-path-error-description =\n    Impossible d'ouvrir \"{ $path }\"\n    \"{ $path }\" n'existe peut-être pas ou vous n'avez pas la permission de l'ouvrir\n\n    Voulez-vous le retirer de la barre latérale ?\nremove = Supprimer\nkeep = Conserver\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Ajouter un lecteur réseau\nconnect = Connecter\nconnect-anonymously = Se connecter anonymement\nconnecting = Connexion...\ndomain = Domaine\nenter-server-address = Entrez l'adresse du serveur\nnetwork-drive-description =\n    Les adresses de serveur incluent un préfixe de protocole et une adresse.\n    Exemples : ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocoles disponibles,Préfixe\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDav,dav:// or davs://\nnetwork-drive-error = Impossible d'accéder au lecteur réseau\npassword = Mot de passe\nremember-password = Se souvenir du mot de passe\ntry-again = Essayer à nouveau\nusername = Nom d'utilisateur\n\n## Operations\n\ncancelled = Annulé\nedit-history = Modifier l'historique\nhistory = Historique\nno-history = Aucun élément dans l'historique.\npending = En attente\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, annulation\nprogress-paused = { $percent }%, en pause\nfailed = Échoué\ncomplete = Terminé\ncompressing =\n    Compression de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis \"{ $from }\" vers \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] élément compressé\n       *[other] éléments compressés\n    } depuis \"{ $from }\" vers \"{ $to }\"\ncopy_noun = Copier\ncreating = Création de \"{ $name }\" dans \"{ $parent }\"\ncreated = \"{ $name }\" créé dans \"{ $parent }\"\ncopying =\n    Copie de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis \"{ $from }\" vers \"{ $to }\" ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] élément copié\n       *[other] éléments copiés\n    } depuis \"{ $from }\" vers \"{ $to }\"\ndeleting =\n    Suppression de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis { trash } ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] élément supprimé\n       *[other] éléments supprimés\n    } depuis { trash }\nemptying-trash = { trash } en cours de nettoyage ({ $progress })...\nemptied-trash = { trash } vidée\nextracting =\n    Extraction de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis \"{ $from }\" vers \"{ $to }\" ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] élément extrait\n       *[other] éléments extraits\n    } depuis \"{ $from }\" vers \"{ $to }\"\nsetting-executable-and-launching = Paramétrage de \"{ $name }\" comme exécutable et prêt à être lancé\nset-executable-and-launched = Défini \"{ $name }\" comme exécutable et lancé\nmoving =\n    Déplacement de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis \"{ $from }\" vers \"{ $to }\" ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] élément déplacé\n       *[other] éléments déplacés\n    } de \"{ $from }\" à \"{ $to }\"\npermanently-deleting =\n    Suppression définitive de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    }\npermanently-deleted =\n    { $items } { $items ->\n        [one] élément supprimé\n       *[other] éléments supprimés\n    } définitivement\nrenaming = Renommage de \"{ $from }\" en \"{ $to }\"\nrenamed = \"{ $from }\" renommé en \"{ $to }\"\nrestoring =\n    Restauration de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } depuis la { trash } ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] élément restauré\n       *[other] éléments restaurés\n    } depuis la { trash }\nunknown-folder = Dossier inconnu\n\n## Open with\n\nmenu-open-with = Ouvrir avec...\ndefault-app = { $name } (défaut)\n\n## Show details\n\nshow-details = Afficher les détails\ntype = Type : { $mime }\nitems = Éléments : { $items }\nitem-size = Taille : { $size }\nitem-created = Créé : { $created }\nitem-modified = Modifié : { $modified }\nitem-accessed = Consulté : { $accessed }\ncalculating = Calcul en cours...\n\n## Settings\n\nsettings = Paramètres\nsingle-click = Ouvrir en un clic\n\n### Appearance\n\nappearance = Apparence\ntheme = Thème\nmatch-desktop = Assortir au bureau\ndark = Sombre\nlight = Clair\n\n### Type to Search\n\ntype-to-search = Tapez pour rechercher\ntype-to-search-recursive = Recherche dans le dossier actuel et tous les sous-dossiers\ntype-to-search-enter-path = Entrez le chemin du dossier ou du fichier\n# Context menu\nadd-to-sidebar = Ajouter à la barre latérale\ncompress = Compresser...\ndelete-permanently = Supprimer définitivement\nextract-here = Extraire\nnew-file = Nouveau fichier...\nnew-folder = Nouveau dossier...\nopen-in-terminal = Ouvrir dans le terminal\nmove-to-trash = Déplacer vers la corbeille\nrestore-from-trash = Restaurer depuis la corbeille\nremove-from-sidebar = Supprimer de la barre latérale\nsort-by-name = Trier par nom\nsort-by-modified = Trier par date de modification\nsort-by-size = Trier par taille\nsort-by-trashed = Trier par date de suppression\n\n## Desktop\n\nchange-wallpaper = Changer le fond d'écran...\ndesktop-appearance = Apparence du bureau...\ndisplay-settings = Paramètres d'affichage...\n\n# Menu\n\n\n## File\n\nfile = Fichier\nnew-tab = Nouvel onglet\nnew-window = Nouvelle fenêtre\nrename = Renommer...\nclose-tab = Fermer l'onglet\nquit = Quitter\n\n## Edit\n\nedit = Modifier\ncut = Couper\ncopy = Copier\npaste = Coller\nselect-all = Sélectionner tout\n\n## View\n\nzoom-in = Zoomer\ndefault-size = Taille par défaut\nzoom-out = Dézoomer\nview = Affichage\ngrid-view = Vue en grille\nlist-view = Vue en liste\nshow-hidden-files = Afficher les fichiers cachés\nlist-directories-first = Lister les répertoires en premier\ngallery-preview = Aperçu de la galerie\nmenu-settings = Paramètres...\nmenu-about = À propos de Fichiers COSMIC...\n\n## Sort\n\nsort = Trier\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Le plus récent en premier\nsort-oldest-first = Le plus ancien en premier\nsort-smallest-to-largest = Du plus petit au plus grand\nsort-largest-to-smallest = Du plus grand au plus petit\nreload-folder = Actualiser le dossier\nrelated-apps = Applications associées\nremoving-from-recents =\n    Suppression de { $items } { $items ->\n        [one] élément\n       *[other] éléments\n    } de { recents }\nother-apps = Autres applications\nset-permissions = Définir les permissions pour \"{ $name }\" à { $mode }\nrepository = Dépôt\nsupport = Support\neject = Éjecter\nremove-from-recents = Supprimer des récents\nempty-trash-title = Vider la corbeille ?\nsetting-permissions = Définition des permissions pour \"{ $name }\" à { $mode }\nremoved-from-recents =\n    { $items } { $items ->\n        [one] élément supprimé\n       *[other] éléments supprimés\n    } de { recents }\nprogress-failed = { $percent }%, échec\ntype-to-search-select = Sélectionne le premier fichier ou dossier qui convient\npasted-image = Image collée\npasted-text = Text collé\npasted-video = Vidéo collée\ncopy-to-title = Sélectionner la destination de la copie\ncopy-to-button-label = Copier\nmove-to-title = Sélectionner la destination du déplacement\nmove-to-button-label = Déplacer\ncopy-to = Copier vers...\nmove-to = Déplacer vers...\ncomment = Explorateur de fichiers pour le bureau COSMIC\nkeywords = Dossier;Gestionnaire;\nshow-recents = Dossier Récents dans la barre latérale\ncopy-path = Copier le chemin\nclear-recents-history = Effacer l'historique des Récents\nmixed = Mixte\n"
  },
  {
    "path": "i18n/fy/cosmic_files.ftl",
    "content": "support = Stipe\nrepository = Argyf\nopen-file = Iepenje in bestân\ncancel = Annulearje\nopen-folder = Iepenje in map\nsettings = Ynstellings\nmatch-desktop = Systeemstandert\ncosmic-files = COSMIC Bestannen\nempty-folder = Lege map\nempty-folder-hidden = Lege map (mei ferburgen bestannen)\nno-results = Gjin resultaten fûn\n"
  },
  {
    "path": "i18n/ga/cosmic_files.ftl",
    "content": "cosmic-files = Comhaid COSMIC\nempty-folder = Fillteán folamh\nempty-folder-hidden = Fillteán folamh (tá míreanna folaithe ann)\nno-results = Níor aimsíodh aon torthaí\nfilesystem = Córas comhad\nhome = Baile\nnetworks = Líonraí\nnotification-in-progress = Tá oibríochtaí comhaid ar siúl\ntrash = Bruscar\nrecents = Le Déanaí\nundo = Cuir ar ceal\ntoday = Inniu\n# Desktop view options\ndesktop-view-options = Roghanna radhairc deisce...\nshow-on-desktop = Taispeáin ar an deasc\ndesktop-folder-content = Ábhar fillteáin deisce\nmounted-drives = Tiomántáin mhonaithe\ntrash-folder-icon = Deilbhín fillteáin bruscair\nicon-size-and-spacing = Méid agus spásáil na ndeilbhíní\nicon-size = Méid na ndeilbhíní\ngrid-spacing = Spásáil an ghreille\n# List view\nname = Ainm\nmodified = Mionathraithe\ntrashed-on = Curtha sa bhruscar\nsize = Méid\n# Progress footer\ndetails = Sonraí\ndismiss = Diúltaigh an teachtaireacht\noperations-running =\n    { $running } { $running ->\n        [one] oibríocht\n       *[other] oibríochtaí\n    } ag rith ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] oibríocht\n       *[other] oibríochtaí\n    } ag rith ({ $percent }%), { $finished } críochnaithe...\npause = Sos\nresume = Tosaigh arís\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Cruthaigh cartlann\n\n## Extract Dialog\n\nextract-password-required = Pasfhocal riachtanach\nextract-to = Asbhain go...\nextract-to-title = Asbhain go fillteán\n\n## Empty Trash Dialog\n\nempty-trash = Folmhaigh an bruscar\nempty-trash-warning = Scriosfar míreanna sa bhfillteán Bruscair go buan\n\n## Mount Error Dialog\n\nmount-error = Ní féidir rochtain a fháil ar an tiomántán\n\n## New File/Folder Dialog\n\ncreate-new-file = Cruthaigh comhad nua\ncreate-new-folder = Cruthaigh fillteán nua\nfile-name = Ainm comhaid\nfolder-name = Ainm fillteáin\nfile-already-exists = Tá comhad leis an ainm sin ann cheana féin\nfolder-already-exists = Tá fillteán leis an ainm sin ann cheana féin\nname-hidden = Beidh ainmneacha ag tosú le \".\" i bhfolach\nname-invalid = Ní féidir an t-ainm a bheith \"{ $filename }\"\nname-no-slashes = Ní féidir slaiseanna a bheith san ainm\n\n## Open/Save Dialog\n\ncancel = Cealaigh\ncreate = Cruthaigh\nopen = Oscail\nopen-file = Oscail comhad\nopen-folder = Oscail fillteán\nopen-in-new-tab = Oscail i gcluaisín nua\nopen-in-new-window = Oscail i bhfuinneog nua\nopen-item-location = Oscail suíomh na míre\nopen-multiple-files = Oscail ilchomhaid\nopen-multiple-folders = Oscail ilfhillteáin\nsave = Sábháil\nsave-file = Sábháil comhad\n\n## Open With Dialog\n\nopen-with-title = Conas is mian leat \"{ $name }\" a oscailt?\nbrowse-store = Brabhsáil { $store }\n\n## Rename Dialog\n\nrename-file = Athainmnigh comhad\nrename-folder = Athainmnigh fillteán\n\n## Replace Dialog\n\nreplace = Cuir in ionad\nreplace-title = Tá \"{ $filename }\" ann sa suíomh seo cheana féin\nreplace-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.\nreplace-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.\noriginal-file = Comhad bunaidh\nreplace-with = Cuir in ionad le\napply-to-all = Cuir i bhfeidhm ar gach ceann\nkeep-both = Coinnigh an dá cheann\nskip = Scipeáil\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Socraigh mar inrite agus lainseáil\nset-executable-and-launch-description = Ar mhaith leat \"{ $name }\" a shocrú mar chomhad inrite agus é a lainseáil?\nset-and-launch = Socraigh agus lainseáil\n\n## Metadata Dialog\n\nopen-with = Oscail le\nowner = Úinéir\ngroup = Grúpa\nother = Eile\n\n### Mode 0\n\nnone = Dada\n\n### Mode 1 (unusual)\n\nexecute-only = Inrite amháin\n\n### Mode 2 (unusual)\n\nwrite-only = Scríobh amháin\n\n### Mode 3 (unusual)\n\nwrite-execute = Scríobh agus inrite\n\n### Mode 4\n\nread-only = Léamh amháin\n\n### Mode 5\n\nread-execute = Léamh agus inrite\n\n### Mode 6\n\nread-write = Léamh agus scríobh\n\n### Mode 7\n\nread-write-execute = Léamh, scríobh, agus inrite\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Earráid ag oscailt an eolaire\nfavorite-path-error-description =\n    Ní féidir \"{ $path }\" a oscailt\n    B'fhéidir nach bhfuil \"{ $path }\" ann nó b'fhéidir nach bhfuil cead agat é a oscailt\n\n    Ar mhaith leat é a bhaint den bharra taoibh?\nremove = Bain\nkeep = Coimeád\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Cuir tiomántán líonra leis\nconnect = Ceangail\nconnect-anonymously = Ceangail gan ainm\nconnecting = Ag ceangal...\ndomain = Fearann\nenter-server-address = Cuir isteach seoladh an fhreastalaí\nnetwork-drive-description =\n    Áirítear le seoltaí freastalaí réimír prótacail agus seoladh.\n    Samplaí: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Prótacail atá ar fáil, Réimír\n    AppleTalk,afp://\n    Prótacal Aistrithe Comhad,ftp:// nó ftps://\n    Córas Comhad Líonra,nfs://\n    Bloc Teachtaireachtaí Freastalaí,smb://\n    Prótacal Aistrithe Comhad SSH,sftp:// nó ssh://\n    WebDav,dav:// nó davs://\nnetwork-drive-error = Ní féidir teacht ar an tiomántán líonra\npassword = Pasfhocal\nremember-password = Cuimhnigh pasfhocal\ntry-again = Bain triail eile as\nusername = Ainm úsáideora\n\n## Operations\n\ncancelled = Cealaithe\nedit-history = Cuir stair in eagar\nhistory = Stair\nno-history = Gan aon mhíreanna sa stair.\npending = Ar feitheamh\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, cealaithe\nprogress-paused = Curtha ar shos ag { $percent }%\nfailed = Theip\ncomplete = Críochnaithe\ncompressing =\n    Á chomhbhrú { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\" ({ $progress })...\ncompressed =\n    Comhbhrúdh { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\"\ncopy_noun = Cóipeáil\ncreating = Á chruthú \"{ $name }\" i \"{ $parent }\"\ncreated = Cruthaíodh \"{ $name }\" i \"{ $parent }\"\ncopying =\n    Ag cóipeáil { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\" ({ $progress })...\ncopied =\n    Cóipeáilte { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\"\ndeleting =\n    Ag scriosadh { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { trash } ({ $progress })...\ndeleted =\n    Scriosta { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { trash }\nemptying-trash = Ag folmhú { trash } ({ $progress })...\nemptied-trash = Folmhaíodh an { trash }\nextracting =\n    Ag asbhaint { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\" ({ $progress })...\nextracted =\n    Asbhainte { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\"\nsetting-executable-and-launching = Ag socrú \"{ $name }\" mar inrite agus ag lainseáil\nset-executable-and-launched = Socraigh \"{ $name }\" mar inrite agus lainseáilte\nmoving =\n    Ag bogadh { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\" ({ $progress })...\nmoved =\n    Bogadh { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó \"{ $from }\" go \"{ $to }\"\nrenaming = Ag athainmniú \"{ $from }\" go \"{ $to }\"\nrenamed = Athainmnithe \"{ $from }\" go \"{ $to }\"\nrestoring =\n    Ag athchóiriú{ $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { trash } ({ $progress })...\nrestored =\n    Athchóirithe { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { trash }\nunknown-folder = Fillteán anaithnid\n\n## Open with\n\nmenu-open-with = Oscail le...\ndefault-app = { $name } (réamhshocraithe)\n\n## Show details\n\nshow-details = Taispeáin sonraí\ntype = Cineál: { $mime }\nitems = Míreanna: { $items }\nitem-size = Méid: { $size }\nitem-created = Cruthaithe: { $created }\nitem-modified = Mionathraithe: { $modified }\nitem-accessed = Rochtainte: { $accessed }\ncalculating = Á ríomh...\n\n## Settings\n\nsettings = Socruithe\nsingle-click = Cliceáil amháin le hoscailt\n\n### Appearance\n\nappearance = Cuma\ntheme = Téama\nmatch-desktop = Meaitseáil deasc\ndark = Dorcha\nlight = Solas\n\n### Type to Search\n\ntype-to-search = Clóscríobh le cuardach\ntype-to-search-recursive = Cuardaíonn sé an fillteán reatha agus na fo-fhillteáin go léir\ntype-to-search-enter-path = Iontrálann sé seo an cosán chuig an eolaire nó an comhad\n# Context menu\nadd-to-sidebar = Cuir leis an mbarra taoibh\ncompress = Comhbhrúigh...\ndelete-permanently = Scrios go buan\nextract-here = Asbhain\nnew-file = Comhad nua...\nnew-folder = Fillteán nua...\nopen-in-terminal = Oscail sa teirminéal\nmove-to-trash = Bog go dtí an bruscar\nrestore-from-trash = Athchóirigh ón mbruscar\nremove-from-sidebar = Bain ón mbarra taoibh\nsort-by-name = Sórtáil de réir ainm\nsort-by-modified = Sórtáil de réir modhnaithe\nsort-by-size = Sórtáil de réir méid\nsort-by-trashed = Sórtáil de réir am scriosta\n\n## Desktop\n\nchange-wallpaper = Athraigh cúlbhrat...\ndesktop-appearance = Cuma deisce...\ndisplay-settings = Socruithe taispeána...\n\n# Menu\n\n\n## File\n\nfile = Comhad\nnew-tab = Cluaisín nua\nnew-window = Fuinneog nua\nrename = Athainmnigh...\nclose-tab = Dún cluaisín\nquit = Scoir\n\n## Edit\n\nedit = Eagar\ncut = Gearr\ncopy = Cóipeáil\npaste = Greamaigh\nselect-all = Roghnaigh gach ceann\n\n## View\n\nzoom-in = Súmáil isteach\ndefault-size = Méid réamhshocraithe\nzoom-out = Súmáil amach\nview = Amharc\ngrid-view = Amharc greille\nlist-view = Amharc liosta\nshow-hidden-files = Taispeáin comhaid fholaithe\nlist-directories-first = Liostaigh eolairí ar dtús\ngallery-preview = Réamhamharc gailearaí\nmenu-settings = Socruithe...\nmenu-about = Maidir le Comhaid COSMIC...\n\n## Sort\n\nsort = Sórtáil\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Is nuaí ar dtús\nsort-oldest-first = Is sine ar dtús\nsort-smallest-to-largest = Ón gceann is lú go dtí an ceann is mó\nsort-largest-to-smallest = Ón gceann is mó go dtí an ceann is lú\nrepository = Stór\nsupport = Tacaíocht\nother-apps = Feidhmchláir eile\nrelated-apps = Feidhmchláir ghaolmhara\nselected-items = Na { $items } míreanna roghnaithe\npermanently-delete-question = Scriosadh go buan?\ndelete = Scrios\npermanently-delete-warning = Scriosfar { $target } go buan. Ní féidir an gníomh seo a chealú.\nprogress-failed = Theip ar { $percent }%\nsetting-permissions = Ceadanna á socrú do \"{ $name }\" go { $mode }\nset-permissions = Socraigh ceadanna do \"{ $name }\" go { $mode }\npermanently-deleting =\n    Ag scriosadh go buan { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    }\npermanently-deleted =\n    Scriosta go buan { $items } { $items ->\n        [one] mhír amháin\n       *[other] míreanna\n    }\nremoving-from-recents =\n    Ag baint { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { recents }\nremoved-from-recents =\n    Bainte { $items } { $items ->\n        [one] mhír\n       *[other] míreanna\n    } ó { recents }\neject = Díchuir\nremove-from-recents = Bain as na cinn is déanaí\nreload-folder = Athlódáil an fillteán\nempty-trash-title = Folmhaigh an bruscar?\ntype-to-search-select = Roghnaíonn an chéad chomhad nó fillteán comhoiriúnach\npasted-image = Íomhá ghreamaithe\npasted-text = Téacs greamaithe\npasted-video = Físeán greamaithe\ncopy-to-title = Roghnaigh ceann scríbe cóipeála\ncopy-to-button-label = Cóipeáil\nmove-to-title = Roghnaigh ceann scríbe an bhogtha\nmove-to-button-label = Bog\ncopy-to = Cóipeáil chuig...\nmove-to = Bog go...\ncomment = Bainisteoir comhad don deasc COSMIC\nkeywords = Fillteán;Bainisteoir;\nshow-recents = Fillteán le déanaí sa bharra taoibh\nclear-recents-history = Glan stair na n-earraí le déanaí\ncopy-path = Cóipeáil an chosán\nmixed = Measctha\n"
  },
  {
    "path": "i18n/gd/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/gu/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/he/cosmic_files.ftl",
    "content": "connect = התחברות\nprogress = { $percent }%\n"
  },
  {
    "path": "i18n/hi/cosmic_files.ftl",
    "content": "cosmic-files = कॉस्मिक फाइल्स\nempty-folder = खाली फ़ोल्डर\nempty-folder-hidden = खाली फ़ोल्डर (अदृश्य आइटम शामिल हैं)\nno-results = कोई परिणाम नहीं\nfilesystem = फाइल सिस्टम\nhome = होम\nnetworks = नेटवर्क्स\nnotification-in-progress = फाइल संचालन प्रगति पर है।\ntrash = कचरा\nrecents = हाल के\nundo = पूर्ववत करें\ntoday = आज\n# Desktop view options\ndesktop-view-options = डेस्कटॉप दृश्य विकल्प...\nshow-on-desktop = डेस्कटॉप पर दिखाएं\ndesktop-folder-content = डेस्कटॉप फ़ोल्डर सामग्री\nmounted-drives = माउंट किए गए ड्राइव\ntrash-folder-icon = कचरा फ़ोल्डर आइकन\nicon-size-and-spacing = आइकन आकार और अंतर\nicon-size = आइकन आकार\n# List view\nname = नाम\nmodified = संशोधित तिथि\ntrashed-on = कचरे में डालने की तिथि\nsize = आकार\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = संग्रह बनाएँ\n\n## Empty Trash Dialog\n\nempty-trash = कचरा खाली करें\nempty-trash-warning = क्या आप वाकई कचरे में सभी आइटमों को स्थायी रूप से हटाना चाहते हैं?\n\n## New File/Folder Dialog\n\ncreate-new-file = नई फाइल बनाएँ\ncreate-new-folder = नया फ़ोल्डर बनाएँ\nfile-name = फाइल का नाम\nfolder-name = फ़ोल्डर का नाम\nfile-already-exists = इसी नाम की फाइल पहले से मौजूद है।\nfolder-already-exists = इसी नाम का फ़ोल्डर पहले से मौजूद है।\nname-hidden = \".\" से शुरू होने वाले नाम छिपे रहेंगे।\nname-invalid = नाम \"{ $filename }\" मान्य नहीं है।\nname-no-slashes = नाम में स्लैश का उपयोग नहीं किया जा सकता है।\n\n## Open/Save Dialog\n\ncancel = रद्द करें\ncreate = बनाएँ\nopen = खोलें\nopen-file = फ़ाइल खोलें\nopen-folder = फ़ोल्डर खोलें\nopen-in-new-tab = नई टैब में खोलें\nopen-in-new-window = नई विंडो में खोलें\nopen-item-location = आइटम का स्थान खोलें\nopen-multiple-files = कई फ़ाइलें खोलें\nopen-multiple-folders = कई फ़ोल्डर खोलें\nsave = सहेजें\nsave-file = फ़ाइल सहेजें\n\n## Open With Dialog\n\nopen-with-title = \"{ $name }\" को कैसे खोलना चाहेंगे?\nbrowse-store = { $store } में ब्राउज़ करें\n\n## Rename Dialog\n\nrename-file = फाइल का नाम बदलें\nrename-folder = फ़ोल्डर का नाम बदलें\n\n## Replace Dialog\n\nreplace = प्रतिस्थापित करें\nreplace-title = { $filename } पहले से इस स्थान पर मौजूद है।\nreplace-warning = क्या आप इसे प्रतिस्थापित करना चाहते हैं? यदि प्रतिस्थापित किया गया, तो मौजूदा फ़ाइल को ओवरराइट किया जाएगा।\nreplace-warning-operation = क्या आप इसे बदलना चाहते हैं? प्रतिस्थापित करने पर मौजूदा फ़ाइल ओवरराइट हो जाएगी।\noriginal-file = मूल फ़ाइल\nreplace-with = इसके साथ प्रतिस्थापित करें\napply-to-all = सभी पर लागू करें\nkeep-both = दोनों रखें\nskip = छोड़ें\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = निष्पादन योग्य के रूप में सेट करें और लॉन्च करें\nset-executable-and-launch-description = क्या आप निष्पादन योग्य के रूप में \"{ $name }\" सेट करना चाहते हैं और इसे लॉन्च करते हैं?\nset-and-launch = सेट करें और लॉन्च करें\n\n## Metadata Dialog\n\nowner = मालिक\ngroup = समूह\nother = अन्य\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = नेटवर्क ड्राइव जोड़ें\nconnect = कनेक्ट करें\nconnect-anonymously = गुमनाम रूप से कनेक्ट करें\nconnecting = कनेक्ट हो रहा है...\ndomain = डोमेन\nenter-server-address = सर्वर का पता दर्ज करें\nnetwork-drive-description =\n    सर्वर पते प्रोटोकॉल उपसर्ग और पते सहित होते हैं।\n    उदाहरण: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    उपलब्ध प्रोटोकॉल,उपसर्ग\n    एप्पलटॉक,afp://\n    फ़ाइल ट्रांसफ़र प्रोटोकॉल,ftp:// या ftps://\n    नेटवर्क फ़ाइल सिस्टम,nfs://\n    सर्वर संदेश ब्लॉक,smb://\n    SSH फ़ाइल ट्रांसफ़र प्रोटोकॉल,sftp:// या ssh://\n    वेबDAV,dav:// या davs://\nnetwork-drive-error = नेटवर्क ड्राइव तक पहुंचने में असमर्थ\npassword = पासवर्ड\nremember-password = पासवर्ड याद रखें\ntry-again = फिर से कोशिश करें\nusername = उपयोगकर्ता नाम\n\n## Operations\n\nedit-history = संपादन इतिहास\nhistory = इतिहास\nno-history = इतिहास में कोई आइटम नहीं\npending = लंबित\nfailed = विफल\ncomplete = पूर्ण\ncompressing =\n    संपीड़ित किया जा रहा है { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to }\ncompressed =\n    संपीड़ित किया गया { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to }\ncopy_noun = नकल\ncreating = { $parent } में { $name } बनाया जा रहा है\ncreated = { $parent } में { $name } बनाया गया\ncopying =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } नकल की जा रही है\ncopied =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } नकल की गई\nemptying-trash = कचरा खाली किया जा रहा है\nemptied-trash = कचरा खाली किया गया\nextracting =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } निकाला जा रहा है\nextracted =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } निकाला गया\nsetting-executable-and-launching = \"{ $name }\" को कार्यान्वयन के रूप में सेट किया जा रहा है और लॉन्च किया जा रहा है\nset-executable-and-launched = \"{ $name }\" को कार्यान्वयन के रूप में सेट किया गया है और लॉन्च किया गया है\nmoving =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } स्थानांतरित किया जा रहा है\nmoved =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } से { $from } तक { $to } स्थानांतरित किया गया\nrenaming = { $from } से { $to } तक नाम बदला जा रहा है\nrenamed = { $from } से { $to } तक नाम बदला गया\nrestoring =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } को कचरे से पुनर्स्थापित किया जा रहा है\nrestored =\n    { $items } { $items ->\n        [one] आइटम\n       *[other] आइटम\n    } को कचरे से पुनर्स्थापित किया गया\nunknown-folder = अज्ञात फ़ोल्डर\n\n## Open with\n\nmenu-open-with = इसके साथ खोलें\ndefault-app = { $name } (डिफ़ॉल्ट)\n\n## Show details\n\nshow-details = विवरण दिखाएँ\n\n## Settings\n\nsettings = सेटिंग\n\n### Appearance\n\nappearance = रूप\ntheme = थीम\nmatch-desktop = सिस्टम सेटिंग्स से मेल खाएँ\ndark = डार्क\nlight = लाइट\n# Context menu\nadd-to-sidebar = साइडबार में जोड़ें\ncompress = संपीड़ित करें\nextract-here = यहाँ निकालें\nnew-file = नई फ़ाइल...\nnew-folder = नया फ़ोल्डर...\nopen-in-terminal = टर्मिनल में खोलें\nmove-to-trash = कचरे में भेजें\nrestore-from-trash = कचरे से पुनर्स्थापित करें\nremove-from-sidebar = साइडबार से निकालें\nsort-by-name = नाम से क्रमबद्ध करें\nsort-by-modified = संशोधित तिथि द्वारा क्रमबद्ध करें\nsort-by-size = आकार द्वारा क्रमबद्ध करें\nsort-by-trashed = कचरे में डालने की तिथि द्वारा क्रमबद्ध करें\n\n## Desktop\n\nchange-wallpaper = वॉलपेपर बदलें...\ndesktop-appearance = डेस्कटॉप रूप...\ndisplay-settings = डिस्प्ले सेटिंग्स...\n\n# Menu\n\n\n## File\n\nfile = फ़ाइल\nnew-tab = नया टैब\nnew-window = नई विंडो\nrename = नाम बदलें...\nclose-tab = टैब बंद करें\nquit = बंद करें\n\n## Edit\n\nedit = संपादित करें\ncut = कट करें\ncopy = नकल करें\npaste = चिपकाएँ\nselect-all = सभी चुनें\n\n## View\n\nzoom-in = ज़ूम इन करें\ndefault-size = डिफ़ॉल्ट आकार\nzoom-out = ज़ूम आउट करें\nview = दृश्य\ngrid-view = ग्रिड दृश्य\nlist-view = सूची दृश्य\nshow-hidden-files = छिपी हुई फाइलें दिखाएँ\nlist-directories-first = सबसे पहले डाइरेक्ट्री दिखाएँ\nmenu-settings = सेटिंग्स...\nmenu-about = कॉस्मिक फाइल्स के बारे में...\n\n## Sort\n\nsort = क्रमबद्ध करें\nsort-a-z = अ-ह क्रम में क्रमबद्ध करें\nsort-z-a = ह-अ क्रम में क्रमबद्ध करें\nsort-newest-first = नए से पुराने\nsort-oldest-first = पुराने से नए\nsort-smallest-to-largest = छोटे से बड़े\nsort-largest-to-smallest = बड़े से छोटे\nrepository = रिपॉजिटरी\nsupport = सहायता\n"
  },
  {
    "path": "i18n/hr/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/hu/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Fájlok\ncomment = Fájlkezelő a COSMIC asztali környezethez\nkeywords = mappa;fájl;kezelő;\nempty-folder = Üres mappa\nempty-folder-hidden = Üres mappa (Rejtett elemeket tartalmaz)\nno-results = Nincs találat\nfilesystem = Fájlrendszer\nhome = Saját mappa\nnetworks = Hálózatok\nnotification-in-progress = A fájlműveletek folyamatban vannak\ntrash = Kuka\nrecents = Legutóbbiak\nundo = Visszavonás\ntoday = Ma\n# Desktop view options\ndesktop-view-options = Asztali nézet beállításai…\nshow-on-desktop = Megjelenítés az asztalon\ndesktop-folder-content = Asztal mappa tartalma\nmounted-drives = Csatolt meghajtók\ntrash-folder-icon = Kuka ikonja\nicon-size-and-spacing = Ikonméret és távolság\nicon-size = Ikonméret\ngrid-spacing = Rácsköz\n# List view\nname = Név\nmodified = Módosítva\ntrashed-on = Kukába helyezve\nsize = Méret\n# Progress footer\ndetails = Részletek\ndismiss = Üzenet bezárása\noperations-running =\n    { $running } { $running ->\n        [one] művelet\n       *[other] művelet\n    } fut ({ $percent }%)…\noperations-running-finished =\n    { $running } { $running ->\n        [one] művelet\n       *[other] művelet\n    } fut ({ $percent }%), { $finished } befejeződött…\npause = Szünet\nresume = Folytatás\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Tömörített fájl létrehozása\n\n## Extract Dialog\n\nextract-password-required = Jelszó szükséges\nextract-to = Kibontás ide…\nextract-to-title = Kibontás mappába\n\n## Empty Trash Dialog\n\nempty-trash = Kuka ürítése\nempty-trash-warning = A kukában lévő összes elem véglegesen törölve lesz\n\n## Mount Error Dialog\n\nmount-error = Nem érhető el a meghajtó\n\n## New File/Folder Dialog\n\ncreate-new-file = Új fájl létrehozása\ncreate-new-folder = Új mappa létrehozása\nfile-name = Fájlnév\nfolder-name = Mappa neve\nfile-already-exists = Már létezik ilyen nevű fájl\nfolder-already-exists = Már létezik ilyen nevű mappa\nname-hidden = A ponttal kezdődő nevek rejtve lesznek\nname-invalid = A név nem lehet „{ $filename }”\nname-no-slashes = A név nem tartalmazhat „/” jelet\n\n## Open/Save Dialog\n\ncancel = Mégse\ncreate = Létrehozás\nopen = Megnyitás\nopen-file = Fájl megnyitása\nopen-folder = Mappa megnyitása\nopen-in-new-tab = Megnyitás új lapon\nopen-in-new-window = Megnyitás új ablakban\nopen-item-location = Útvonal megnyitása\nopen-multiple-files = Több fájl megnyitása\nopen-multiple-folders = Több mappa megnyitása\nsave = Mentés\nsave-file = Fájl mentése\n\n## Open With Dialog\n\nopen-with-title = Hogyan szeretnéd megnyitni a következőt: „{ $name }”?\nbrowse-store = { $store } böngészése\nother-apps = Egyéb alkalmazások\nrelated-apps = Hasonló alkalmazások\n\n## Permanently delete Dialog\n\nselected-items = A(z) { $items } kijelölt elem\npermanently-delete-question = Végleges törlés?\ndelete = Törlés\npermanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem visszavonható.\n\n## Rename Dialog\n\nrename-file = Fájl átnevezése\nrename-folder = Mappa átnevezése\n\n## Replace Dialog\n\nreplace = Csere\nreplace-title = „{ $filename }” már létezik\nreplace-warning = Szeretnéd lecserélni a meglévő fájlt a mentendő fájllal? A cserével felülírod annak tartalmát.\nreplace-warning-operation = Szeretnéd lecserélni? A csere felülírja annak tartalmát.\noriginal-file = Eredeti fájl\nreplace-with = Csere erre\napply-to-all = Alkalmazás mindegyikre\nkeep-both = Mindkettő megtartása\nskip = Kihagyás\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Végrehajthatóvá tétel és indítás\nset-executable-and-launch-description = Szeretnéd végrehajthatóvá tenni a(z) „{ $name }” fájlt és elindítani?\nset-and-launch = Beállítás és indítás\n\n## Metadata Dialog\n\nopen-with = Megnyitás ezzel\nowner = Tulajdonos\ngroup = Csoport\nother = Többi\n\n### Mode 0\n\nnone = Nincs\n\n### Mode 1 (unusual)\n\nexecute-only = Csak végrehajtás\n\n### Mode 2 (unusual)\n\nwrite-only = Csak írás\n\n### Mode 3 (unusual)\n\nwrite-execute = Írás és végrehajtás\n\n### Mode 4\n\nread-only = Csak olvasás\n\n### Mode 5\n\nread-execute = Olvasás és végrehajtás\n\n### Mode 6\n\nread-write = Olvasás és írás\n\n### Mode 7\n\nread-write-execute = Olvasás, írás és végrehajtás\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Hiba a könyvtár megnyitásakor\nfavorite-path-error-description =\n    Nem sikerült megnyitni ezt: „{ $path }”\n    „{ $path }” lehet nem létezik, vagy nincs jogosultságod a megnyitásához\n\n    Szeretnéd eltávolítani az oldalsávról?\nremove = Eltávolítás\nkeep = Megtartás\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Hálózati meghajtó hozzáadása\nconnect = Csatlakozás\nconnect-anonymously = Csatlakozás névtelenül\nconnecting = Csatlakozás…\ndomain = Tartomány\nenter-server-address = Add meg a kiszolgáló címét\nnetwork-drive-description =\n    A kiszolgálócímek tartalmazzák a protokoll előtagját és a címet.\n    Például: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Elérhető protokollok,Előtag\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// vagy ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// vagy ssh://\n    WebDAV,dav:// vagy davs://\nnetwork-drive-error = Nem érhető el a hálózati meghajtó\npassword = Jelszó\nremember-password = Jelszó megjegyzése\ntry-again = Újra\nusername = Felhasználónév\n\n## Operations\n\ncancelled = Megszakítva\nedit-history = Fájlműveleti előzmények\nhistory = Előzmények\nno-history = Nem találhatók elemek az előzményekben\npending = Függőben\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, megszakítva\nprogress-paused = { $percent }%, szüneteltetve\nfailed = Sikertelen\ncomplete = Befejezett\ncompressing =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } tömörítése innen: „{ $from }” ide: „{ $to }” ({ $progress })…\ncompressed =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } tömörítve innen: „{ $from }” ide: „{ $to }”\ncopy_noun = Másolat\ncreating = „{ $name }” létrehozása itt: „{ $parent }”\ncreated = „{ $name }” létrehozva itt: „{ $parent }”\ncopying =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } másolása innen: „{ $from }” ide: „{ $to }” ({ $progress })…\ncopied =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } másolva innen: „{ $from }” ide: „{ $to }”\ndeleting =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } törlése a kukából ({ $progress })…\ndeleted =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } törölve a kukából\nemptying-trash = { trash } kiürítése ({ $progress })…\nemptied-trash = { trash } kiürítve\nextracting =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } kibontása innen: „{ $from }” ide: „{ $to }” ({ $progress })…\nextracted =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } kibontva innen: „{ $from }” ide: „{ $to }”\nsetting-executable-and-launching = „{ $name }” végrehajthatóvá tétele és futtatása\nset-executable-and-launched = „{ $name }” végrehajthatóvá lett téve és futtatva\nsetting-permissions = „{ $name }” jogosultságainak beállítása: { $mode }\nset-permissions = „{ $name }” jogosultságai beállítva: { $mode }\nmoving =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } áthelyezése innen: „{ $from }” ide: „{ $to }” ({ $progress })…\nmoved =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } áthelyezve innen: „{ $from }” ide: „{ $to }”\npermanently-deleting =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } végleges törlése\npermanently-deleted =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } véglegesen törölve\nremoving-from-recents =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } eltávolítása a { recents }ból\nremoved-from-recents =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } eltávolítva a { recents }ból\nrenaming = „{ $from }” átnevezése erre: „{ $to }”\nrenamed = „{ $from }” átnevezve erre: „{ $to }”\nrestoring =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } visszaállítása a kukából ({ $progress })…\nrestored =\n    { $items } { $items ->\n        [one] elem\n       *[other] elem\n    } visszaállítva a kukából\nunknown-folder = ismeretlen mappa\n\n## Open with\n\nmenu-open-with = Megnyitás mással…\ndefault-app = { $name } (alapértelmezett)\n\n## Show details\n\nshow-details = Részletek megjelenítése\ntype = Típus: { $mime }\nitems = Elemek: { $items }\nitem-size = Méret: { $size }\nitem-created = Létrehozva: { $created }\nitem-modified = Módosítva: { $modified }\nitem-accessed = Hozzáférve: { $accessed }\ncalculating = Számítás…\n\n## Settings\n\nsettings = Beállítások\nsingle-click = Egykattintásos megnyitás\n\n### Appearance\n\nappearance = Megjelenés\ntheme = Téma\nmatch-desktop = Rendszertéma\ndark = Sötét\nlight = Világos\n\n### Type to Search\n\ntype-to-search = Gépeléssel keresés\ntype-to-search-recursive = Keresés a jelenlegi mappában és almappákban\ntype-to-search-enter-path = Elérési út megadása\n# Context menu\nadd-to-sidebar = Hozzáadás az oldalsávhoz\ncompress = Tömörítés…\ndelete-permanently = Végleges törlés\neject = Kiadás\nextract-here = Kibontás\nnew-file = Új fájl…\nnew-folder = Új mappa…\nopen-in-terminal = Megnyitás a terminálban\nmove-to-trash = Kukába helyezés\nrestore-from-trash = Visszaállítás a kukából\nremove-from-sidebar = Eltávolítás az oldalsávról\nsort-by-name = Név szerinti rendezés\nsort-by-modified = Módosítás szerinti rendezés\nsort-by-size = Méret szerinti rendezés\nsort-by-trashed = Törlés ideje szerinti rendezés\nremove-from-recents = Eltávolítás a legutóbbiak közül\n\n## Desktop\n\nchange-wallpaper = Háttérkép cseréje…\ndesktop-appearance = Asztali megjelenés…\ndisplay-settings = Képernyő-beállítások…\n\n# Menu\n\n\n## File\n\nfile = Fájl\nnew-tab = Új lap\nnew-window = Új ablak\nreload-folder = Mappa újratöltése\nrename = Átnevezés…\nclose-tab = Lap bezárása\nquit = Kilépés\n\n## Edit\n\nedit = Szerkesztés\ncut = Kivágás\ncopy = Másolás\npaste = Beillesztés\nselect-all = Összes kijelölése\n\n## View\n\nzoom-in = Nagyítás\ndefault-size = Alapértelmezett méret\nzoom-out = Kicsinyítés\nview = Nézet\ngrid-view = Rácsnézet\nlist-view = Listanézet\nshow-hidden-files = Rejtett fájlok megjelenítése\nlist-directories-first = Könyvtárak listázása először\ngallery-preview = Galéria előnézet\nmenu-settings = Beállítások…\nmenu-about = A COSMIC Fájlok névjegye…\n\n## Sort\n\nsort = Rendezés\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Legújabb előre\nsort-oldest-first = Legrégibb előre\nsort-smallest-to-largest = Legkisebbtől a legnagyobbig\nsort-largest-to-smallest = Legnagyobbtól a legkisebbig\nrepository = Tároló\nsupport = Támogatás\nprogress-failed = { $percent }%, sikertelen\nempty-trash-title = Kuka ürítése?\ntype-to-search-select = Kijelöli az első egyező fájlt vagy mappát\npasted-image = Beillesztett kép\npasted-text = Beillesztett szöveg\npasted-video = Beillesztett videó\ncopy-to-title = Másolási cél kiválasztása\ncopy-to-button-label = Másolás\nmove-to-title = Áthelyezési cél kiválasztása\nmove-to-button-label = Áthelyezés\ncopy-to = Másolás ide…\nmove-to = Áthelyezés ide…\nshow-recents = Legutóbbiak mappa megjelenítése az oldalsávban\ncopy-path = Útvonal másolása\nclear-recents-history = Legutóbbiak előzményének törlése\nmixed = Vegyes\n"
  },
  {
    "path": "i18n/id/cosmic_files.ftl",
    "content": "empty-folder = Map kosong\nempty-folder-hidden = Map kosong (memiliki item tersembunyi)\nno-results = Tidak ada hasil yang ditemukan\nfilesystem = Sistem berkas\ncosmic-files = Berkas COSMIC\nhome = Beranda\nnetworks = Jaringan\nnotification-in-progress = Operasi berkas sedang berlangsung\ntrash = Sampah\nrecents = Terbaru\nundo = Batalkan\ntoday = Hari ini\ndesktop-view-options = Opsi tampilan desktop...\nshow-on-desktop = Tampilkan di Desktop\ndesktop-folder-content = Konten map desktop\nmounted-drives = Drive terpasang\ntrash-folder-icon = Ikon map sampah\nicon-size-and-spacing = Ukuran dan jarak ikon\nicon-size = Ukuran ikon\nname = Nama\ngrid-spacing = Jarak antar kisi\nmodified = Dimodifikasi\ntrashed-on = Dibuang\nsize = Ukuran\ndetails = Rincian\ndismiss = Abaikan pesan\noperations-running =\n    { $running } { $running ->\n        [one] operasi\n       *[other] operasi\n    } berjalan ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operasi\n       *[other] operasi\n    } berjalan ({ $percent }%), { $finished } selesai...\npause = Jeda\nresume = Lanjutkan\ncreate-archive = Buat arsip\nextract-password-required = Kata sandi diperlukan\nextract-to = Ekstrak ke...\nextract-to-title = Ekstrak ke map\nempty-trash = Kosongkan sampah\nempty-trash-title = Kosongkan sampah?\nempty-trash-warning = Item di map Sampah akan dihapus secara permanen\nemptying-trash = Mengosongkan { trash } ({ $progress })...\nmount-error = Tidak dapat mengakses drive\ncreate-new-file = Buat berkas baru\ncreate-new-folder = Buat map baru\npermanently-delete-question = Hapus secara permanen?\ndelete = Hapus\nsort-by-trashed = Urutkan berdasarkan waktu penghapusan\ndelete-permanently = Hapus secara permanen\npermanently-delete-warning = { $target } akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.\ndeleted =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dihapus dari { trash }\npermanently-deleted =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dihapus secara permanen\nfile-name = Nama berkas\nfolder-name = Nama map\nfile-already-exists = Berkas dengan nama tersebut sudah ada\nfolder-already-exists = Map dengan nama tersebut sudah ada\nname-hidden = Nama yang diawali dengan \".\" akan disembunyikan\nname-invalid = Nama tidak boleh \"{ $filename }\"\nname-no-slashes = Nama tidak boleh berisi garis miring\ncancel = Batalkan\ncreate = Buat\nopen = Buka\nopen-file = Buka berkas\nopen-folder = Buka map\nopen-in-new-tab = Buka di tab baru\nopen-in-new-window = Buka di jendela baru\nopen-item-location = Buka lokasi item\nopen-multiple-files = Buka beberapa berkas\nopen-multiple-folders = Buka beberapa map\nsave = Simpan\nsave-file = Simpan berkas\nopen-with-title = Bagaimana anda ingin membuka \"{ $name }\"?\nbrowse-store = Telusuri { $store }\nother-apps = Aplikasi lainnya\nrelated-apps = Aplikasi terkait\nrename-file = Ganti nama berkas\nrename-folder = Ganti nama map\nreplace = Ganti\nreplace-title = \"{ $filename }\" sudah ada di lokasi ini\nreplace-warning-operation = Apakah anda ingin menggantinya? Menggantinya akan menimpa konten tersebut.\noriginal-file = Berkas asli\nreplace-with = Ganti dengan\napply-to-all = Terapkan ke semua\nreplace-warning = Apakah anda ingin menggantinya dengan yang sedang anda simpan? Menggantinya akan menimpa konten tersebut.\nkeep-both = Pertahankan keduanya\nskip = Lewati\nset-executable-and-launch = Atur sebagai dijalankan dan luncurkan\nset-and-launch = Atur dan luncurkan\nset-executable-and-launch-description = Apakah anda ingin mengatur \"{ $name }\" sebagai dijalankan dan luncurkan?\nopen-with = Buka dengan\nowner = Pemilik\ngroup = Grup\nother = Lainnya\nnone = Tidak ada\nexecute-only = Hanya jalankan\nwrite-only = Hanya tulis\nwrite-execute = Tulis dan jalankan\nread-only = Hanya baca\nread-execute = Baca dan jalankan\nread-write = Baca dan tulis\nread-write-execute = Baca, tulis, dan jalankan\nfavorite-path-error = Galat membuka direktori\nremove = Hapus\nkeep = Pertahankan\nrepository = Repositori\nfavorite-path-error-description =\n    Tidak dapat membuka \"{ $path }\"\n    \"{ $path }\" mungkin tidak ada atau anda mungkin tidak memiliki izin untuk membuka\n\n    Apakah anda ingin menghapus dari bilah sisi?\nsupport = Dukungan\nadd-network-drive = Tambahkan drive jaringan\nconnect = Sambungkan\nconnect-anonymously = Sambungkan secara anonim\nconnecting = Menyambungkan...\ndomain = Domain\nenter-server-address = Masukkan alamat server\nnetwork-drive-description =\n    Alamat server mencakup awalan protokol dan alamat.\n    Contoh: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Protokol yang tersedia,Awalan\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// atau ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// atau ssh://\n    WebDAV,dav:// atau davs://\nnetwork-drive-error = Tidak dapat mengakses drive jaringan\npassword = Kata sandi\nremember-password = Ingat kata sandi\ntry-again = Coba lagi\nusername = Nama pengguna\ncancelled = Dibatalkan\nedit-history = Sunting riwayat\nhistory = Riwayat\nno-history = Tidak ada item dalam riwayat.\npending = Menunggu\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, dibatalkan\nprogress-failed = { $percent }%, gagal\nprogress-paused = { $percent }%, dijeda\nfailed = Gagal\ncomplete = Selesai\ncopy_noun = Salin\ncreating = Membuat \"{ $name }\" di \"{ $parent }\"\ncreated = \"{ $name }\" dibuat di \"{ $parent }\"\ncompressing =\n    Mengompres { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari \"{ $from }\" ke \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dikompres dari \"{ $from }\" ke \"{ $to }\"\ncopied =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } disalin dari \"{ $from }\" ke \"{ $to }\"\ncopying =\n    Menyalin { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari \"{ $from }\" ke \"{ $to }\" ({ $progress })...\ndeleting =\n    Menghapus { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari { trash } ({ $progress })...\nemptied-trash = { trash } telah dikosongkan\nextracting =\n    Mengekstrak { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari \"{ $from }\" ke \"{ $to }\" ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } diekstrak dari \"{ $from }\" ke \"{ $to }\"\nsetting-executable-and-launching = Mengatur \"{ $name }\" sebagai dijalankan dan meluncurkan\nset-executable-and-launched = Atur \"{ $name }\" sebagai dijalankan dan diluncurkan\nsetting-permissions = Mengatur izin untuk \"{ $name }\" ke { $mode }\nset-permissions = Atur izin untuk \"{ $name }\" ke { $mode }\nmenu-open-with = Buka dengan...\nunknown-folder = map yang tidak diketahui\ndefault-app = { $name } (bawaan)\nshow-details = Tampilkan rincian\ntype = Jenis: { $mime }\nitems = Item: { $items }\nitem-size = Ukuran: { $size }\nmoving =\n    Memindahkan { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari \"{ $from }\" ke \"{ $to }\" ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dipindahkan dari \"{ $from }\" ke \"{ $to }\"\npermanently-deleting =\n    Menghapus { $items } { $items ->\n        [one] item\n       *[other] item\n    } secara permanen\nremoving-from-recents =\n    Menghapus { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari { recents }\nremoved-from-recents =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dihapus dari { recents }\nrenaming = Mengganti nama \"{ $from }\" ke \"{ $to }\"\nrenamed = Nama diganti \"{ $from }\" ke \"{ $to }\"\nrestoring =\n    Memulihkan { $items } { $items ->\n        [one] item\n       *[other] item\n    } dari { trash } ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] item\n       *[other] item\n    } dipulihkan dari { trash }\nitem-created = Dibuat: { $created }\nitem-modified = Dimodifikasi: { $modified }\nitem-accessed = Diakses: { $accessed }\ncalculating = Menghitung...\nsettings = Pengaturan\nsingle-click = Klik sekali untuk membuka\nappearance = Tampilan\ntheme = Tema\nmatch-desktop = Cocokkan desktop\ndark = Gelap\nlight = Terang\ntype-to-search = Ketik untuk mencari\ntype-to-search-recursive = Mencari di map saat ini dan semua submap\ntype-to-search-enter-path = Memasukkan jalur ke direktori atau berkas\nadd-to-sidebar = Tambahkan ke bilah sisi\ncompress = Kompres...\neject = Keluarkan\nextract-here = Ekstrak\nnew-file = Berkas baru...\nnew-folder = Map baru...\nopen-in-terminal = Buka di terminal\nmove-to-trash = Pindahkan ke sampah\nrestore-from-trash = Pulihkan dari sampah\nremove-from-sidebar = Hapus dari bilah sisi\nsort-by-name = Urutkan berdasarkan nama\nsort-by-modified = Urutkan berdasarkan dimodifikasi\nsort-by-size = Urutkan berdasarkan ukuran\nremove-from-recents = Hapus dari terbaru\nchange-wallpaper = Ubah wallpaper...\ndesktop-appearance = Tampilan desktop...\ndisplay-settings = Pengaturan layar...\nfile = Berkas\nnew-tab = Tab baru\nnew-window = Jendela baru\nreload-folder = Muat ulang map\nrename = Ganti nama...\nclose-tab = Tutup tab\nquit = Keluar\nedit = Sunting\ncut = Potong\ncopy = Salin\npaste = Tempel\nselect-all = Pilih semua\nzoom-in = Perbesar\ndefault-size = Ukuran bawaan\nzoom-out = Perkecil\nview = Tampilan\ngrid-view = Tampilan kisi\nlist-view = Tampilan daftar\ngallery-preview = Tampilan galeri\nshow-hidden-files = Tampilkan berkas tersembunyi\nlist-directories-first = Daftar direktori terlebih dahulu\nmenu-settings = Pengaturan...\nmenu-about = Tentang Berkas COSMIC...\nsort = Urutkan\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Terbaru terlebih dahulu\nsort-oldest-first = Tertua terlebih dahulu\nsort-smallest-to-largest = Terkecil hingga terbesar\nsort-largest-to-smallest = Terbesar hingga terkecil\nselected-items = { $items } item yang dipilih\ntype-to-search-select = Memilih berkas atau map pertama yang cocok\npasted-image = Gambar Ditempel\npasted-text = Teks Ditempel\npasted-video = Video Ditempel\nmove-to = Pindahkan ke...\ncopy-to = Salin ke...\nmove-to-button-label = Pindahkan\nmove-to-title = Pilih destinasi pindahan\ncopy-to-button-label = Salin\ncopy-to-title = Pilih destinasi salinan\nkeywords = Map;Pengelola;\ncomment = Pengelola berkas untuk desktop COSMIC\nshow-recents = Map terbaru di bilah sisi\nclear-recents-history = Bersihkan riwayat Terbaru\ncopy-path = Salin jalur\nmixed = Bercampur\n"
  },
  {
    "path": "i18n/ie/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/is/cosmic_files.ftl",
    "content": "cancel = Hætta við\nsupport = Stuðningur\ndelete = Eyða\nname = Heiti\nsettings = Stillingar\nappearance = Útlit\ntheme = Þema\nmatch-desktop = Passa við skjáborð\ndark = Dökkt\nlight = Ljóst\nfile = Skrá\nnew-tab = Nýr flipi\nnew-window = Nýr gluggi\nclose-tab = Loka flipa\nquit = Hætta\nedit = Breyta\ncopy = Afrita\npaste = Líma\nselect-all = Velja allt\npassword = Lykilorð\nskip = Sleppa\ncosmic-files = COSMIC Skráastjóri\nempty-folder = Tóm mappa\nempty-folder-hidden = Tóm mappa (inniheldur falin atriði)\nno-results = Engar niðurstöður fundust\nfilesystem = Skráakerfi\nhome = Heimamappa\nnetworks = Net\nnotification-in-progress = Skráaaðgerðir er í gangi.\ntrash = Rusl\nrecents = Nýlegt\nundo = Afturkalla\ntoday = Í dag\ndesktop-view-options = Valkostir skjáborðsyfirlits...\nshow-on-desktop = Sýna á skjáborði\ndesktop-folder-content = Innihald skjáborðsmöppu\nmounted-drives = Tengd drif\ntrash-folder-icon = Tákn ruslamöppu\nicon-size-and-spacing = Stærð og bil tákna\nicon-size = Táknastærð\ngrid-spacing = Bil í reitayfirliti\nmodified = Breytt\ntrashed-on = Sett í rusl\nsize = Stærð\ndetails = Upplýsingar\ndismiss = Hunsa skilaboð\noperations-running =\n    { $running } { $running ->\n        [one] aðgerð\n       *[other] aðgerðir\n    } í gangi ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] aðgerð\n       *[other] aðgerðir\n    } í gangi ({ $percent }%), { $finished } lokið...\npause = Á pásu\nresume = Halda áfram\ncreate-archive = Búa til safnskrá\nextract-password-required = Lykilorðs krafist\nextract-to = Afþjappa í...\nextract-to-title = Afþjappa í möppu\nempty-trash = Tæma rusl\nempty-trash-warning = Viltu örugglega eyða endanlega öllum atriðunum í Ruslmöppunni?\nmount-error = Get ekki opnað drif\ncreate-new-file = Búa til nýja skrá\ncreate-new-folder = Búa til nýja möppu\nfile-name = Skrárheiti\nfolder-name = Möppuheiti\nfile-already-exists = Skrá með þessu heiti er til nú þegar.\nfolder-already-exists = Mappa með þessu heiti er til nú þegar.\nname-hidden = Heiti sem byrja á „.“ verða falin.\nname-invalid = Heiti má ekki vera „{ $filename }“.\nname-no-slashes = Heiti má ekki innihalda skrástrik.\ncreate = Búa til\nopen = Opna\nopen-file = Opna skrá\nopen-folder = Opna möppu\nopen-in-new-tab = Opna í nýjum flipa\nopen-in-new-window = Opna í nýjum glugga\nopen-item-location = Opna staðsetningu atriðis\nopen-multiple-files = Opna margar skrár\nopen-multiple-folders = Opna margar möppur\nsave = Vista\nsave-file = Vista skrá\nopen-with-title = Hvernig viltu opna „{ $name }“?\nbrowse-store = Skoða { $store }\nother-apps = Önnur forrit\nrelated-apps = Tengd forrit\nselected-items = völdu atriðin { $items }\npermanently-delete-question = Eyða varanlega\npermanently-delete-warning = Viltu örugglega eyða { $target } varanlega? Það er ekki hægt að afturkalla.\nrename-file = Endurnefna skrá\nrename-folder = Endurnefna möppu\nreplace = Skipta út\nreplace-title = „{ $filename }“ er nú þegar til á þessum stað.\nreplace-warning = Viltu skipta henni út fyrir þá sem þú ert að vista? Ef henni er skipt út verður skrifað yfir innihald hennar.\nreplace-warning-operation = Viltu skipta henni út? Ef henni er skipt út verður skrifað yfir innihald hennar.\noriginal-file = Upprunaleg skrá\nreplace-with = Skipta út fyrir\napply-to-all = Nota á allt\nkeep-both = Halda báðum\nset-executable-and-launch = Gera að keyrsluskrá og keyra\nset-executable-and-launch-description = Viltu gera „{ $name }“ að keyrsluskrá og keyra hana?\nset-and-launch = Stilla og keyra\nopen-with = Opna með\nowner = Eigandi\ngroup = Hópur\nother = Annað\nnone = Ekkert\nread-only = Skrifvarin\nread-write = Lesa og skrifa\nread-write-execute = Lesa, skrifa og keyra\nfavorite-path-error = Villa við að opna möppu\nfavorite-path-error-description =\n    Gat ekki opnað „{ $path }“.\n    Kannski er hún ekki til eða þú hefur ekki heimild til að opna hana.\n\n    Viltu fjarlægja hana úr hliðarstikunni?\nremove = Fjarlægja\nkeep = Geyma\nadd-network-drive = Bæta við netdrifi\nconnect = Tengjast\nconnect-anonymously = Tengjast nafnlaust\nconnecting = Tengist…\ndomain = Lén\nenter-server-address = Sláðu inn vistfang þjóns\nnetwork-drive-description =\n    Vistföng þjóns innihalda forskeyti og vistfang.\n    Dæmi: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Tiltækir samskiptastaðlar,forskeyti\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// eða ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// eða ssh://\n    WebDAV,dav:// eða davs://\nnetwork-drive-error = Gat ekki opnað netdrif\nremember-password = Muna lykilorð\ntry-again = Reyna aftur\nusername = Notandanafn\ncancelled = Hætt við\nhistory = Ferill\nno-history = Engin atriði í ferilskrá.\nprogress-cancelled = { $percent }%, hætt við\nprogress-failed = { $percent }%, mistókst\nprogress-paused = { $percent }%, á pásu\nfailed = Mistókst\ncomplete = Lokið\ncompressing =\n    Þjappa { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“ ({ $progress })...\ncompressed =\n    Þjappaði { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“\ncopy_noun = Afrita\ncreating = Bý til „{ $name }“ í „{ $parent }“\ncreated = Bjó til „{ $name }“ í „{ $parent }“\nexecute-only = Keyra eingöngu\nwrite-only = Aðeins skrifréttindi\nwrite-execute = Skrif- og keyrsluréttindi\nread-execute = Les- og keyrsluréttindi\nrepository = Hugbúnaðarsafn\nedit-history = Breytingaferill\npending = Í bið\ncopying =\n    Afrita { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“ ({ $progress })...\ncopied =\n    Afritaði { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“\ndeleting =\n    Eyði { $items } { $items ->\n        [one] atriði\n       *[other] atriðum\n    } úr { trash } ({ $progress })...\ndeleted =\n    Eyddi { $items } { $items ->\n        [one] atriði\n       *[other] atriðum\n    } úr { trash }\nemptying-trash = Tæmi { trash } ({ $progress })...\nemptied-trash = Tæmdi { trash }\nextracting =\n    Afþjappa { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“ ({ $progress })...\nextracted =\n    Afþjappaði { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“\nsetting-executable-and-launching = Stillingin „{ $name }“ er keyranleg og að ræsast\nset-executable-and-launched = Stilla „{ $name }“ sem keyranlega og ræst\nsetting-permissions = Stilli heimildir fyrir „{ $name }“ á { $mode }\nunknown-folder = óþekkt mappa\nmenu-open-with = Opna með...\ndefault-app = { $name } (sjálfgefið)\nshow-details = Sýna upplýsingar\ntype = Gerð: { $mime }\nitems = Atriði: { $items }\nitem-size = Stærð: { $size }\nitem-created = Búið til: { $created }\nitem-modified = Breytt: { $modified }\nitem-accessed = Opnað: { $accessed }\ncalculating = Reikna...\nsingle-click = Smella einu sinni til að opna\ntype-to-search = Slá inn til að leita\ntype-to-search-recursive = Leitar í núverandi möppu og öllum undirmöppum\ntype-to-search-enter-path = Setur inn slóðina að möppunni eða skránni\nadd-to-sidebar = Bæta við hliðarstiku\ncompress = Þjappa\ndelete-permanently = Eyða varanlega\neject = Ýta út\nextract-here = Afþjappa\nnew-file = Ný skrá...\nnew-folder = Ný mappa...\nopen-in-terminal = Opna í skjáhermi\nmove-to-trash = Færa í ruslið\nrestore-from-trash = Endurheimta úr ruslinu\nremove-from-sidebar = Fjarlægja af hliðarstiku\nsort-by-name = Raða eftir heiti\nsort-by-modified = Raða eftir breytingadegi\nsort-by-size = Raða eftir stærð\nsort-by-trashed = Raða eftir eyðingartíma\nremove-from-recents = Fjarlægja úr nýlegu\nchange-wallpaper = Skipta um veggfóður...\ndesktop-appearance = Útlit skjáborðs...\ndisplay-settings = Skjástillingar...\nreload-folder = Endurhlaða möppu\nrename = Endurnefna...\ncut = Klippa\nzoom-in = Auka aðdrátt\ndefault-size = Sjálfgefin stærð\nzoom-out = Minnka aðdrátt\nview = Skoða\ngrid-view = Reitayfirlit\nlist-view = Listayfirlit\nshow-hidden-files = Sýna faldar skrár\nlist-directories-first = Hafa möppur fremst\ngallery-preview = Forskoðun myndasafns\nmenu-settings = Stillingar...\nmenu-about = Um COSMIC skráastjórann...\nsort = Raða\nsort-a-z = A-Ö\nsort-z-a = Ö-A\nsort-newest-first = Nýjast fyrst\nsort-oldest-first = Elst fyrst\nsort-smallest-to-largest = Minnsta til stærsta\nsort-largest-to-smallest = Stærsta til minnsta\nprogress = { $percent }%\nset-permissions = Stilla heimildir fyrir „{ $name }“ á { $mode }\nmoving =\n    Færi { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“ ({ $progress })...\nmoved =\n    Færði { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr „{ $from }“ í „{ $to }“\npermanently-deleting =\n    Eyði varanlega { $items } { $items ->\n        [one] atriði\n       *[other] atriðum\n    }\npermanently-deleted =\n    Eyddi varanlega { $items } { $items ->\n        [one] atriði\n       *[other] atriðum\n    }\nremoving-from-recents =\n    Fjarlægi { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr { recents }\nremoved-from-recents =\n    Fjarlægði { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr { recents }\nrenaming = Endurnefni „{ $from }“ í „{ $to }“\nrenamed = Endurnefndi „{ $from }“ í „{ $to }“\nrestoring =\n    Endurheimti { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr { trash } ({ $progress })...\nrestored =\n    Endurheimti { $items } { $items ->\n        [one] atriði\n       *[other] atriði\n    } úr { trash }\n"
  },
  {
    "path": "i18n/it/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Files\ncomment = File manager di COSMIC\nkeywords = File;Archivi;Cartelle;Explorer;\nempty-folder = Cartella vuota\nempty-folder-hidden = Cartella vuota (con elementi nascosti)\nno-results = Nessun risultato trovato\nfilesystem = Filesystem\nhome = Home\nnetworks = Reti\nnotification-in-progress = Operazioni sui file in corso.\ntrash = Cestino\nrecents = Recenti\nundo = Annulla\ntoday = Oggi\n# Desktop view options\ndesktop-view-options = Impostazioni visualizzazione Desktop...\nshow-on-desktop = Mostra sul Desktop\ndesktop-folder-content = Contenuto cartella del Desktop\nmounted-drives = Dispositivi montati\ntrash-folder-icon = Icona del cestino\nicon-size-and-spacing = Dimensioni e spaziatura icona\nicon-size = Dimensione icona\ngrid-spacing = Spaziatura griglia\n# List view\nname = Nome\nmodified = Modificato\ntrashed-on = Cestinato\nsize = Dimensione\n# Progress footer\ndetails = Dettagli\ndismiss = Nascondi messaggio\noperations-running =\n    { $running } { $running ->\n        [one] operazione\n       *[other] operazioni\n    } in corso ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operazione\n       *[other] operazioni\n    } in corso ({ $percent }%), { $finished } completata...\npause = Pausa\nresume = Riprendi\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Crea archivio\n\n## Extract Dialog\n\nextract-password-required = Password richiesta\nextract-to = Estrai in...\nextract-to-title = Estrai nella cartella\n\n## Empty Trash Dialog\n\nempty-trash = Svuota cestino\nempty-trash-warning = Sei sicuro di voler eliminare definitivamente tutti gli elementi nel cestino?\n\n## Mount Error Dialog\n\nmount-error = Impossibile accedere al dispositivo\n\n## New File/Folder Dialog\n\ncreate-new-file = Crea un nuovo file\ncreate-new-folder = Crea una nuova cartella\nfile-name = Nome file\nfolder-name = Nome cartella\nfile-already-exists = Esiste già un file con questo nome.\nfolder-already-exists = Esiste già una cartella con questo nome.\nname-hidden = I nomi che iniziano con \".\" verranno nascosti.\nname-invalid = Il nome non può essere \"{ $filename }\".\nname-no-slashes = I nomi non possono contenere gli slash.\n\n## Open/Save Dialog\n\ncancel = Annulla\ncreate = Crea\nopen = Apri\nopen-file = Apri file\nopen-folder = Apri cartella\nopen-in-new-tab = Apri in una nuova scheda\nopen-in-new-window = Apri in una nuova finestra\nopen-item-location = Apri percorso file\nopen-multiple-files = Apri files multipli\nopen-multiple-folders = Apri cartelle multiple\nsave = Salva\nsave-file = Salva file\n\n## Open With Dialog\n\nopen-with-title = Come vuoi aprire il file \"{ $name }\"?\nbrowse-store = Cerca in { $store }\nother-apps = Altre applicazioni\nrelated-apps = Applicazioni simili\n\n## Permanently delete Dialog\n\nselected-items = i { $items } elementi selezionati\npermanently-delete-question = Elimina definitivamente\ndelete = Elimina\npermanently-delete-warning = Sei sicuro di voler eliminare definitivamente { $target }? Questa azione non può essere annullata.\n\n## Rename Dialog\n\nrename-file = Rinomina file\nrename-folder = Rinomina cartella\n\n## Replace Dialog\n\nreplace = Sostituisci\nreplace-title = \"{ $filename }\" esiste già in questo percorso.\nreplace-warning = Vuoi sostituirlo con quello che stai per salvare? La sostituzione sovrascriverà il suo contenuto.\nreplace-warning-operation = Vuoi sostituirlo? La sostituzione sovrascriverà il suo contenuto.\noriginal-file = File originale\nreplace-with = Sostituisci con\napply-to-all = Applica a tutti\nkeep-both = Mantieni entrambi\nskip = Salta\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Imposta come \"eseguibile\" e apri\nset-executable-and-launch-description = Vuoi impostare \"{ $name }\" come \"eseguibile\" e aprirlo?\nset-and-launch = Imposta e apri\n\n## Metadata Dialog\n\nopen-with = Apri con\nowner = Proprietario\ngroup = Gruppo\nother = Altro\n\n### Mode 0\n\nnone = Nessuno\n\n### Mode 1 (unusual)\n\nexecute-only = Sola esecuzione\n\n### Mode 2 (unusual)\n\nwrite-only = Sola scrittura\n\n### Mode 3 (unusual)\n\nwrite-execute = Scrittura ed esecuzione\n\n### Mode 4\n\nread-only = Sola lettura\n\n### Mode 5\n\nread-execute = Lettura e esecuzione\n\n### Mode 6\n\nread-write = Lettura e scrittura\n\n### Mode 7\n\nread-write-execute = Lettura, scrittura ed esecuzione\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Errore nell'apertura della cartella\nfavorite-path-error-description =\n    Impossibile aprire \"{ $path }\".\n    Potrebbe non esistere o potresti non avere i permessi di accesso.\n\n    Vuoi rimuoverla dalla barra laterale?\nremove = Rimuovi\nkeep = Mantieni\n\n# Context Pages\n\n\n## About\n\nrepository = Repository\nsupport = Supporto\n\n## Add Network Drive\n\nadd-network-drive = Aggiungi dispositivo di rete\nconnect = Connetti\nconnect-anonymously = Connetti in modo anonimo\nconnecting = Connessione in corso...\ndomain = Dominio\nenter-server-address = Inserisci indirizzo di rete\nnetwork-drive-description =\n    Gli indirizzi dei server includono il prefisso del protocollo e l'indirizzo.\n    Esempi: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocolli disponibili,Prefisso\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDav,dav:// or davs://\nnetwork-drive-error = Impossibile accedere al dispositivo di rete\npassword = Password\nremember-password = Ricorda password\ntry-again = Riprova\nusername = Nome utente\n\n## Operations\n\ncancelled = Annullato\nedit-history = Modifica cronologia\nhistory = Cronologia\nno-history = Nessun elemento nella cronologia.\npending = In coda\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, annullato\nprogress-paused = { $percent }%, in pausa\nfailed = Fallito\ncomplete = Completato\ncompressing =\n    Compressione in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncompressed =\n    Compressi { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\"\ncopy_noun = Copia\ncreating = Creazione \"{ $name }\" in \"{ $parent }\"\ncreated = Creato \"{ $name }\" in \"{ $parent }\"\ncopying =\n    Copia in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\" ({ $progress })...\ncopied =\n    Copiati { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\"\ndeleting =\n    Eliminazione in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } dal { trash }: ({ $progress })...\ndeleted =\n    Eliminati { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } dal { trash }\nemptying-trash = Svuotamento del { trash }: ({ $progress })...\nemptied-trash = { trash } svuotato\nextracting =\n    Estrazione in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\": ({ $progress })...\nextracted =\n    Estratti { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\"\nsetting-executable-and-launching = Impostazione in corso di \"{ $name }\" come \"eseguibile\" e avvio\nset-executable-and-launched = Impostato \"{ $name }\" come \"eseguibile\" e avviato\nmoving =\n    Spostamento in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\": ({ $progress })...\nmoved =\n    Spostati { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da \"{ $from }\" a \"{ $to }\"\nrenaming = Rinominazione di \"{ $from }\" in \"{ $to }\"\nrenamed = Rinominato \"{ $from }\" in \"{ $to }\"\nrestoring =\n    Ripristino in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } dal { trash }: ({ $progress })...\nrestored =\n    Ripristinati { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } dal { trash }\nunknown-folder = cartella sconosciuta\n\n## Open with\n\nmenu-open-with = Apri con...\ndefault-app = { $name } (predefinito)\n\n## Show details\n\nshow-details = Mostra dettagli\ntype = Tipo: { $mime }\nitems = Files: { $items }\nitem-size = Dimensione: { $size }\nitem-created = Creato: { $created }\nitem-modified = Modificato in data: { $modified }\nitem-accessed = Accesso eseguito in data: { $accessed }\ncalculating = Calcolo in corso...\n\n## Settings\n\nsettings = Impostazioni\nsingle-click = Click singolo per aprire\n\n### Appearance\n\nappearance = Aspetto\ntheme = Tema\nmatch-desktop = Sistema\ndark = Scuro\nlight = Chiaro\n\n### Type to Search\n\ntype-to-search = Digita per cercare\ntype-to-search-recursive = Cerca nella cartella attuale e nelle sue sotto-cartelle\ntype-to-search-enter-path = Inserisci il percorso della cartella o del file\n# Context menu\nadd-to-sidebar = Aggiungi alla barra laterale\ncompress = Comprimi\ndelete-permanently = Eliminazione definitiva\neject = Espelli\nextract-here = Estrai\nnew-file = Nuovo file...\nnew-folder = Nuova cartella...\nopen-in-terminal = Apri nel terminale\nmove-to-trash = Sposta nel cestino\nrestore-from-trash = Ripristina dal cestino\nremove-from-sidebar = Rimuovi dalla barra laterale\nsort-by-name = Ordina per nome\nsort-by-modified = Ordina per data di modifica\nsort-by-size = Ordina per dimensione\nsort-by-trashed = Ordina per data di eliminazione\nremove-from-recents = Rimuovi da recenti\n\n## Desktop\n\nchange-wallpaper = Modifica sfondo...\ndesktop-appearance = Aspetto del Desktop...\ndisplay-settings = Impostazioni del display...\n\n# Menu\n\n\n## File\n\nfile = File\nnew-tab = Nuova scheda\nnew-window = Nuova finestra\nreload-folder = Aggiorna cartella\nrename = Rinomina...\nclose-tab = Chiudi scheda\nquit = Esci\n\n## Edit\n\nedit = Modifica\ncut = Taglia\ncopy = Copia\npaste = Incolla\nselect-all = Seleziona tutto\n\n## View\n\nzoom-in = Aumenta zoom\ndefault-size = Dimensione predefinita\nzoom-out = Diminuisci zoom\nview = Visualizza\ngrid-view = Visualizzazione a griglia\nlist-view = Visualizzazione a elenco\nshow-hidden-files = Mostra file nascosti\nlist-directories-first = Mostra prima le cartelle\ngallery-preview = Anteprima immagine\nmenu-settings = Impostazioni...\nmenu-about = Informazioni su COSMIC Files...\n\n## Sort\n\nsort = Ordina\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Prima i più recenti\nsort-oldest-first = Prima i più vecchi\nsort-smallest-to-largest = Dal più piccolo al più grande\nsort-largest-to-smallest = Dal più grande al più piccolo\nprogress-failed = { $percent }%, fallito\nsetting-permissions = Impostazione dei permessi per \"{ $name }\" su { $mode }\nset-permissions = Permessi impostati per \"{ $name }\" su { $mode }\npermanently-deleting =\n    Eliminazione definitva in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    }\npermanently-deleted =\n    Eliminazione definitivamente { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    }\nremoving-from-recents =\n    Rimozione in corso di { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da { recents }\nremoved-from-recents =\n    Rimossi { $items } { $items ->\n        [one] elemento\n       *[other] elementi\n    } da { recents }\n"
  },
  {
    "path": "i18n/ja/cosmic_files.ftl",
    "content": "cosmic-files = COSMICファイル\nempty-folder = 空のフォルダ\nempty-folder-hidden = 空のフォルダ（隠しファイルあり）\nno-results = 検索結果はありません\nfilesystem = ファイルシステム\nhome = ホーム\nnetworks = ネットワーク\nnotification-in-progress = ファイル処理が進行中です。\ntrash = ゴミ箱\nrecents = 最近\nundo = 元に戻す\n# List view\nname = 名前\nmodified = 更新日\nsize = サイズ\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = アーカイブを作成\n\n## Empty Trash Dialog\n\nempty-trash = ゴミ箱を空にする\nempty-trash-warning = ゴミ箱のアイテムをすべて完全に削除してもよろしいですか？\n# New File/Folder Dialog\ncreate-new-file = 新しいファイルを作成\ncreate-new-folder = 新しいフォルダを作成\nfile-name = ファイル名\nfolder-name = フォルダ名\nfile-already-exists = 同じ名前のファイルがすでに存在します。\nfolder-already-exists = 同じ名前のフォルダがすでに存在します。\nname-hidden = 「.」で始まる名前は隠られます。\nname-invalid = 「{ $filename }」という名前は使用できません。\nname-no-slashes = 「/」は名前に含められません。\n# Open/Save Dialog\ncancel = キャンセル\ncreate = 作る\nopen = 開く\nopen-file = ファイルを開く\nopen-folder = フォルダを開く\nopen-in-new-tab = 新しいタブで開く\nopen-in-new-window = 新しいウィンドウで開く\nopen-item-location = アイテムの場所を開く\nopen-multiple-files = 複数ファイルを開く\nopen-multiple-folders = 複数フォルダを開く\nsave = 保存\nsave-file = ファイルを保存\n# Rename Dialog\nrename-file = ファイル名を変更\nrename-folder = フォルダ名を変更\n# Replace Dialog\nreplace = 置き換える\nreplace-title = { $filename }はすでにこの場所に存在します。\nreplace-warning = 保存しているファイルで置き換えますか？置き換えると、内容を上書きます。\nreplace-warning-operation = 置き換えますか？置き換えると、内容を上書きます。\noriginal-file = 元のファイル\nreplace-with = これで置き換える：\napply-to-all = 全てに適用\nkeep-both = 両方を保管\nskip = スキップ\n\n## Metadata Dialog\n\nowner = 所有者\ngroup = グループ\nother = その他\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = ネットワークドライブを追加\nconnect = 接続する\nconnect-anonymously = 匿名的に接続\nconnecting = 接続中...\ndomain = ドメイン\nenter-server-address = サーバーアドレスを入力\nnetwork-drive-description =\n    サーバーアドレスはプロトコル接頭辞とアドレスを含めます。\n    例: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    利用可能なプロトコル,接頭辞\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// または ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// または ssh://\n    WebDav,dav:// または davs://\nnetwork-drive-error = ネットワークドライブにアクセスできませんでした\npassword = パスワード\nremember-password = パスワードを覚える\ntry-again = 再試行\nusername = ユーザー名\n\n## Operations\n\nedit-history = 履歴を編集\nhistory = 履歴\nno-history = 履歴はありません。\npending = 保留中\nfailed = 失敗\ncomplete = 完了\ncompressing =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムを圧縮中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\ncompressed =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムを圧縮しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\ncopy_noun = コピー\ncreating = { $parent }で{ $name }を作成中\ncreated = { $parent }で{ $name }を作成完了\ncopying =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムをコピー中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\ncopied =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムをコピーしました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nemptying-trash = { trash }を空にしています ({ $progress })…\nemptied-trash = { trash }を空にした\nmoving =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムを移動中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\nmoved =\n    \"{ $from }\" から \"{ $to }\" へ { $items }個のアイテムを移動しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nrenaming = { $from }を{ $to }に変更中\nrenamed = { $from }を{ $to }に変更完了\nrestoring =\n    { trash }から{ $items }個のアイテムを復元中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\nrestored =\n    { trash }から{ $items }個のアイテムを復元しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nunknown-folder = 不明なフォルダー\n\n## Open with\n\nmenu-open-with = 別のアプリケーションで開く...\ndefault-app = { $name } (デフォルト)\n\n## Properties\n\n\n## Settings\n\nsettings = 設定\n\n### Appearance\n\nappearance = 外観\ntheme = テーマ\nmatch-desktop = システム設定に従う\ndark = ダーク\nlight = ライト\n# Context menu\nadd-to-sidebar = サイドバーに追加\ncompress = 圧縮\nextract-here = 抽出\nnew-file = 新しいファイル...\nnew-folder = 新しいフォルダ...\nopen-in-terminal = 端末で開く\nmove-to-trash = ゴミ箱に移動\nrestore-from-trash = ゴミ箱から復元\nremove-from-sidebar = サイドバーから削除\nsort-by-name = 名前で並べ替え\nsort-by-modified = 更新日で並べ替え\nsort-by-size = サイズで並べ替え\n\n# Menu\n\n\n## File\n\nfile = ファイル\nnew-tab = 新しいタブ\nnew-window = 新しいウィンドウ\nrename = 名前を変更...\nclose-tab = タブを閉じる\nquit = 終了\n\n## Edit\n\nedit = 編集\ncut = 切り取り\ncopy = コピー\npaste = 貼り付け\nselect-all = すべてを選択\n\n## View\n\nzoom-in = ズームイン\ndefault-size = 規定のサイズ\nzoom-out = ズームアウト\nview = 表示\ngrid-view = グリッドの表示\nlist-view = リストの表示\nshow-hidden-files = 隠しファイルを表示\nlist-directories-first = フォルダを最初に表示\nmenu-settings = 設定...\nmenu-about = COSMICファイルについて...\n\n## Sort\n\nsort = 並べ替え\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = 新しい順\nsort-oldest-first = 古い順\nsort-smallest-to-largest = 最小から最大まで\nsort-largest-to-smallest = 最大から最小まで\nrepository = リポジトリ\nsupport = サポート\nremove = 削除\ntoday = 今日\ndesktop-view-options = デスクトップの表示オプション…\nshow-on-desktop = デスクトップの表示オプション\ndesktop-folder-content = デスクトップフォルダの内容\nmounted-drives = マウント済みドライブ\ntrash-folder-icon = ゴミ箱のアイコン\nicon-size-and-spacing = アイコンのサイズと間隔\nicon-size = アイコンサイズ\ngrid-spacing = グリッドの間隔\ntrashed-on = ゴミ箱に入れた日時\ndetails = 詳細\ndismiss = メッセージを閉じる\noperations-running =\n    { $running } { $running ->\n        [one] 件の操作が\n       *[other] 件の操作が\n    }実行中です({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] 件の操作が\n       *[other] 件の操作が\n    } 実行中です({ $percent }%)、 { $finished } 件が終了...\npause = 一時停止\nresume = 一時停止\nextract-password-required = パスワードが必要です\nextract-to = 展開先…\nextract-to-title = フォルダーに展開\nmount-error = ドライブにアクセスできません\nopen-with-title = 「{ $name }」をどのように開きますか？\nbrowse-store = { $store } を参照\nother-apps = 他のアプリケーション\nrelated-apps = 関連アプリケーション\nselected-items = 選択された{ $items }個のアイテム\npermanently-delete-question = 完全に削除\ndelete = 削除\npermanently-delete-warning = { $target }を完全に削除してもよろしいですか？この操作は元に戻せません。\nset-executable-and-launch = 実行可能にして起動\nset-executable-and-launch-description = \"{ $name }\"を実行可能に設定して起動しますか？\nset-and-launch = 設定して起動\nopen-with = 別のアプリケーションで開く\nnone = なし\nexecute-only = 実行のみ\nwrite-only = 書き込み専用\nwrite-execute = 書き込みと実行\nread-only = 読み取り専用\nread-execute = 読み取りと実行\nread-write = 読み取りと書き込み\nread-write-execute = 読み取り、書き込み、実行\nfavorite-path-error = ディレクトリを開けませんでした\nfavorite-path-error-description =\n    \"{ $path }\"を開けません。\n    このパスが存在しないか、開くための権限がない可能性があります。\n\n    サイドバーから削除しますか？\nkeep = そのままにする\ncancelled = キャンセルされました\nprogress = { $percent } %\nprogress-cancelled = { $percent } %、キャンセルされました\nprogress-failed = { $percent } %、失敗\nprogress-paused = { $percent } %、一時停止中\ndeleting =\n    { $items }個のアイテムを{ trash }から削除中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\ndeleted =\n    { $items } 個のアイテムを { trash } から削除しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nextracting =\n    \"{ $from }\" から \"{ $to }\" へ { $items } 個のアイテムを展開中 ({ $progress }) { $items ->\n        [one] 項目\n       *[other] 項目\n    }…\nextracted =\n    \"{ $from }\" から \"{ $to }\" へ { $items } 個のアイテムを展開しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nsetting-executable-and-launching = \"{ $name }\"を実行可能に設定して起動中\nset-executable-and-launched = \"{ $name }\"を実行可能に設定して起動しました\nsetting-permissions = \"{ $name }\"のパーミッションを{ $mode }に設定中\nset-permissions = \"{ $name }\"のパーミッションを{ $mode }に設定しました\npermanently-deleting =\n    { $items }個のアイテムを完全に削除中 { $items ->\n        [one] 項目\n       *[other] 項目\n    }\npermanently-deleted =\n    { $items }個のアイテムを完全に削除しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nremoving-from-recents =\n    { $items }個のアイテムを{ recents }から削除中 { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nremoved-from-recents =\n    { $items }個のアイテムを{ recents }から削除しました { $items ->\n        [one] 項目\n       *[other] 項目\n    }\nshow-details = 詳細を表示\ntype = 種類: { $mime }\nitems = アイテム: { $items }\nitem-size = サイズ: { $size }\nitem-created = 作成日時: { $created }\nitem-modified = 最終更新日時: { $modified }\nitem-accessed = 最終アクセス日時: { $accessed }\ncalculating = 計算中…\nsingle-click = シングルクリックで開く\ntype-to-search = 入力して検索\ntype-to-search-recursive = 現在のフォルダーとすべてのサブフォルダーを検索\ntype-to-search-enter-path = ディレクトリーまたはファイルのパスを入力\ndelete-permanently = 完全に削除する\neject = 取り出し\nsort-by-trashed = 削除日時\nremove-from-recents = 最近の項目から削除\nchange-wallpaper = 壁紙を変更…\ndesktop-appearance = デスクトップの見た目…\ndisplay-settings = ディスプレイの設定…\nreload-folder = フォルダーを再読み込み\ngallery-preview = ギャラリープレビュー\n"
  },
  {
    "path": "i18n/jv/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/ka/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/kab/cosmic_files.ftl",
    "content": "change-wallpaper = Beddel aɣrab n ugdil…\ncosmic-files = Ifuyla COSMIC\nempty-folder = Akaram d ilem\nempty-folder-hidden = Akaram d ilem (yesɛa iferdisen yeffren)\nno-results = Ulac igmaḍ yettwafen\nhome = Agejdan\nnetworks = Iẓeḍwa\nnotification-in-progress = Timhalin ɣef ifuyla la tteddunt\ntrash = Iḍumman\nrecents = Melmi kan\nundo = Ssemmet\ntoday = Ass-a\ndesktop-view-options = Iɣewwaṛen n tmeẓri n tnarit…\nshow-on-desktop = Sken deg tnarit\ndesktop-folder-content = Agbur n ukaram n tnarit\ntrash-folder-icon = Tignit n ukaram n iḍumman\nicon-size-and-spacing = Tiddi n tignit akked tallunt\nicon-size = Tiddi n tignit\nname = Isem\nmodified = Ittusnifel\ntrashed-on = Yettwakkes ɣer tqecwalt n yiḍumman\nsize = Tiddi\ndetails = Talqayt\npause = Serǧu\nresume = Kemmel\ncreate-archive = Snulfu-d aɣbaṛ\nextract-to = Ssef ɣer...\nextract-to-title = Ssef ɣer ukaram\nempty-trash = Silem iḍumman\nrename-folder = Snifel isem n ukaram\nfilesystem = Anagraw n yifuyla\ndismiss = Zgel izen\nempty-trash-title = Silem iḍumman?\nempty-trash-warning = Iferdisen n ukaram n iḍumman ad ttwakksen i lebda\ncreate-new-file = Snulfu-d afaylu amaynut\ncreate-new-folder = Snulfu-d akaram amaynut\nfile-name = Isem n ufaylu\nfolder-name = Isem n ukaram\nfile-already-exists = Afaylu s yisem-agi yella yakan\nfolder-already-exists = Akaram s yisem-agi yella yakan\nname-hidden = Ismawen ibeddun s \".\" ad ttwaffren\nname-invalid = Isem ur yezmir ara ad yili \"{ $filename }\"\ncancel = Sefsex\ncreate = Snulfu-d\nopen = Ldi\nopen-file = Ldi afaylu\nopen-folder = Ldi akaram\nopen-in-new-tab = Ldi deg yiccer amaynut\nopen-in-new-window = Ldi deg usfaylu amaynut\nopen-item-location = Ldi adig n uferdis\nopen-multiple-files = Ldi aget n ifuyla\nmounted-drives = imeɣriyen yettuserkben\nmount-error = Ulamek anekcum ɣer umeɣri\noperations-running =\n    { $running } { $running ->\n        [one] n temhelt la tteddu\n       *[other] n temhal la tteddunt\n    } ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] n temhelt la tteddu\n       *[other] n temhal la tteddunt\n    } ({ $percent }%), { $finished } { $finished ->\n        [one] tfukk\n       *[other] fukkent\n    }...\ncopy-to-title = Fren taɣerwaḍt n unɣel\ncopy-to-button-label = Nɣel\nmove-to-title = Fren taɣerwaḍt n usmutti\nmove-to-button-label = Smutti\ncomment = Amsefrak n yifuyla i tnarit COSMIC\nkeywords = Akaram;Amsefrak;\ndelete = kkes\nreplace = Semselsi\nsupport = Tallalt\nsettings = Iɣewwaṛen\nappearance = Timeẓri\ntheme = Asentel\ndark = Aɣmayan\nlight = Aceɛlal\npaste = Senteḍ\nselect-all = Fren akk\nzoom-in = Asemɣeṛ\ndefault-size = Tiddi tamezwert\nzoom-out = Asemẓi\nview = Wali\nmenu-settings = Iɣewwaṛen…\nsave = Sekles\nmatch-desktop = Amṣada d tnarit\nfile = Afaylu\nnew-window = Asfaylu amaynut\nquit = Tuffɣa\nedit = Ẓreg\ncut = Gzem\ncopy = Nɣel\nrepository = Asarsay\npasted-image = Tettwasenṭeḍ tugna\npasted-text = Yettwasenṭeḍ uḍris\npasted-video = Tettwasenṭeḍ tvidyut\ncompress = Skussem…\ngrid-spacing = Tallunt n iẓiki\nextract-password-required = Awal uffir yettwasra\npermanently-delete-question = Kkes s wudem imezgi?\npermanently-delete-warning = { $target } ad yettwakkes s wudem imezgi. Tigawt-agi ur tezmir ara ad tettwasefsex.\nrename-file = Snifel isem n ufaylu\nreplace-title = \"{ $filename }\" yella yakan deg wadig-a\nreplace-warning = Tebɣiḍ ad t-tsemselsiḍ s win ara teskelseḍ? Asemselsi-ines ad yaru sennig ugbur-is.\nreplace-warning-operation = Tebɣiḍ ad t-tsemselsiḍ? Asemselsi-ines ad yaru sennig ugbur-is.\noriginal-file = Afaylu aneṣli\nreplace-with = Semselsi s\napply-to-all = Snes i meṛṛa\nkeep-both = Eǧǧ-iten deg sin\nskip = Zgel\nset-executable-and-launch = Sbeddet am umselkam syinna senker\nset-executable-and-launch-description = Tebɣiḍ ad tesbeddeḍ \"{ $name }\" am umselkam syinna ad tessenkreḍ?\nset-and-launch = Sbadu sakin senker\nadd-network-drive = Rnu ameɣri n uẓeṭṭa\nconnect = Qqen\nconnect-anonymously = Qqen s wudem udrig\nconnecting = Tuqqna…\ndomain = Taɣult\nenter-server-address = Sekcem tansa n uqeddac\nnetwork-drive-description =\n    Tansiwin n uqeddac gebrent azwir n uneggaf akked tansa.\n    Imedyaten: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-error = Ur izmir ara ad yekcem ɣer umeɣri n uzeṭṭa\npassword = Awal uffir\nremember-password = Cfu ɣef wawal uffir\ntry-again = ɛreḍ tikelt nniḍen\nusername = Isem n useqdac\ncancelled = Yettwasefsex\nedit-history = Ẓreg amazray\nhistory = Amazray\nno-history = Ulac iferdisen deg umazray.\npending = Yettṛaǧu\nprogress-cancelled = { $percent }%, yettwasefsex\nprogress-failed = { $percent }%, ur yeddi ara\nfailed = Ur yeddi ara\nrenaming = Asnifel n yisem \"{ $from }\" ɣer \"{ $to }\"\nrenamed = Yettwasenfel yisem n \"{ $from }\" ɣer \"{ $to }\"\nunknown-folder = akaram arussin\nmenu-open-with = Ldi s…\ndefault-app = { $name } (amezwer)\nshow-details = Sken talqayt\ntype = Anaw: { $mime }\nitems = Iferdisen: { $items }\nitem-size = Tiddi: { $size }\nitem-created = Yettwarna: { $created }\nitem-modified = Ittusnifel: { $modified }\nitem-accessed = Yettwakcem: { $accessed }\ncalculating = Asiḍen…\nsingle-click = Asiti asuf i ulday\ntype-to-search = Aru iwakken ad tnadiḍ\ntype-to-search-recursive = Nadi akaram amiran akked ikaramen inaddawen meṛṛa\ntype-to-search-enter-path = Sekcem abrid ɣer ukaram neɣ afaylu\nadd-to-sidebar = Rnu ɣer ufeggag adisan\ndelete-permanently = Kkes i lebda\ngrid-view = Askan s iẓiki\nlist-view = Askan s tebdart\nshow-hidden-files = Sken ifuyla uffiren\ngallery-preview = Taskant n temidelt\nmenu-about = Ɣef Ifuyla COSMIC…\nsort = Asmizzwer\nsort-newest-first = Amaynut d amezwaru\nsort-oldest-first = Aqbur d amezwaru\nsort-smallest-to-largest = Seg umeẓyan akk ɣer umeqqran\nsort-largest-to-smallest = Seg umeqqran akk ɣer umeẓyan\nopen-multiple-folders = Ldi usgit n ikaramen\nsave-file = Sekles afaylu\nopen-with-title = Amek tebɣiḍ ad teldiḍ \"{ $name }\"?\nbrowse-store = Snirem { $store }\nother-apps = Isnasen-nniḍen\nemptying-trash = Silem { trash } ({ $progress })…\nemptied-trash = D ilem { trash }\nset-permissions = Sbadu isirigen i \"{ $name }\" ɣer { $mode }\neject = Ḍeqqer\nextract-here = Ssef\nnew-file = Afaylu amaynut…\nnew-folder = Akaram amaynut…\nopen-in-terminal = Ldi deg yixef\nmove-to-trash = Smutti ɣer tqecwalt n yiḍumman\nrestore-from-trash = Err-d seg tqecwalt n yiḍumman\nremove-from-sidebar = Kkes seg ugalis adisan\nsort-by-name = Smizzwer s yisem\nsort-by-modified = Asmizzwer s usnifel\nsort-by-size = Asmizzwer s tiddi\nsort-by-trashed = Asmizzwer s wakud n tukksa\nremove-from-recents = Kkes seg ineggura\ndesktop-appearance = Timeẓri n tnarit…\ndisplay-settings = Iɣewwaṛen n ubeqqeḍ...\nnew-tab = Iccer amaynut\nreload-folder = Ales asali n ukaram\nrename = Snifel isem...\nclose-tab = Mdel iccer\nlist-directories-first = Sken di tazwara ikaramen\nopen-with = Ldi s\nowner = Bab\ngroup = Agraw\nother = Ayen nniḍen\nnone = Ula Yiwen\nexecute-only = Selkem kan\nwrite-only = Aru kan\nwrite-execute = Aru u selkem\nread-only = I tɣuri kan\nread-execute = Ɣeṛ u selkem\nread-write = Ɣeṛ u aru\nread-write-execute = Ɣeṛ, aru, u selkem\nfavorite-path-error = Tuccḍa deg ulday n ukaram\nremove = Kkes\nkeep = Eǧǧ\nprogress = { $percent }%\nprogress-paused = { $percent }%, ibedd\ncomplete = Immed\ncopy_noun = Nɣel\ncreating = Asnulfu n \"{ $name }\" deg \"{ $parent }\"\ncreated = Yettwarna \"{ $name }\" deg \"{ $parent }\"\nsetting-executable-and-launching = Asbadu n \"{ $name }\" am umselkam syin ad yettwasekker\nset-executable-and-launched = Sbadu \"{ $name }\" am umselkam syin sekker-it\nsetting-permissions = Asbadu n tsirag i \"{ $name }\" ɣer { $mode }\nrelated-apps = Isnasen icudden\nfavorite-path-error-description =\n    Ur izmir ara ad yeldi \"{ $path }\"\n    \"{ $path }\" yezmer lḥal ulac-it neɣ ahat ur tesɛiḍ ara tisirag akken ad t-teldiḍ\n\n    Tebɣiḍ ad t-tekkseḍ seg ufeggag adisan?\ncompressing =\n    La issekkussum { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\" ({ $progress })...\ncompressed =\n    Yekussem { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\"\ncopying =\n    Anɣal { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\" ({ $progress })...\ncopied =\n    Yettwanɣel { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\"\ndeleting =\n    Tukksa { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { trash } ({ $progress })...\ndeleted =\n    Yettwakkes { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { trash }\nextracting =\n    Tussfa { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\" ({ $progress })...\nextracted =\n    Yettusef { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\"\nmoving =\n    Asmutti { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\" ({ $progress })...\nmoved =\n    Yettwasenkez { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg \"{ $from }\" ɣer \"{ $to }\"\npermanently-deleting =\n    Tukksa i lebda { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    }\npermanently-deleted =\n    I lebda yettwakkes { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    }\nremoving-from-recents =\n    Tukksa { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { recents }\nremoved-from-recents =\n    Yettwakkes { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { recents }\nrestoring =\n    Tiririt { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { trash } ({ $progress })…\nrestored =\n    Yettwarr-d { $items } { $items ->\n        [one] n uferdis\n       *[other] n yiferdisen\n    } seg { trash }\nnetwork-drive-schemes =\n    Ineggafen iwejden, azwir\n    AppleTalk,afp://\n    Aneggaf n usiweḍ n yifuyla,ftp:// neɣ ftps://\n    Anagraw n yifuyla n uzeṭṭa,nfs://\n    Iḥder n yizen n uqeddac,smb://\n    SSH Aneggaf n usiweḍ n yifuyla,sftp:// neɣ ssh://\n    WebDAV:// neɣ davs://\nsort-a-z = A-Ẓ\nsort-z-a = Ẓ-A\nselected-items = { $items } n yiferdisen yettwafernen\ntype-to-search-select = Fren afaylu amezwaru neɣ akaram yemṣadan\ncopy-to = Nɣel ɣer...\nmove-to = Smutti ɣer…\nshow-recents = Akaram n melmi kan deg ufeggag adisan\nclear-recents-history = Sfeḍ azray n melmi kan\ncopy-path = Nɣel abrid\n"
  },
  {
    "path": "i18n/kk/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC файлдары\nempty-folder = Бос бума\nempty-folder-hidden = Бос бума (жасырын элементтері бар)\nno-results = Нәтижелер табылмады\nfilesystem = Файлдық жүйе\nhome = Үй\nnetworks = Желілер\nnotification-in-progress = Файлдармен әрекеттер орындалуда\ntrash = Қоқыс шелегі\nrecents = Соңғылар\nundo = Болдырмау\ntoday = Бүгін\ndesktop-view-options = Жұмыс үстелінің көрініс опциялары...\nshow-on-desktop = Жұмыс үстелінде көрсету\ndesktop-folder-content = Жұмыс үстелі бумасының мазмұны\nmounted-drives = Тіркелген дискілер\ntrash-folder-icon = Қоқыс шелегі бумасының таңбашасы\nicon-size-and-spacing = Таңбаша өлшемі мен аралықтары\nicon-size = Таңбаша өлшемі\ngrid-spacing = Тор аралықтары\nname = Аты\nmodified = Өзгертілген\ntrashed-on = Қоқыс шелегіне тасталған\nsize = Өлшемі\ndetails = Ақпараты\ndismiss = Хабарламаны елемеу\noperations-running =\n    { $running } { $running ->\n        [one] әрекет\n       *[other] әрекет\n    } орындалуда ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] әрекет\n       *[other] әрекет\n    } орындалуда ({ $percent }%), { $finished } аяқталды...\npause = Аялдату\nresume = Жалғастыру\ncreate-archive = Архив жасау\nextract-password-required = Пароль керек\nextract-to = Шығару...\nextract-to-title = Бумаға шығару\nempty-trash = Себетті тазарту\nempty-trash-title = Себетті тазарту керек пе?\nempty-trash-warning = Себет бумасындағы элементтер біржола өшіріледі\nmount-error = Дискіге қол жеткізу мүмкін емес\ncreate-new-file = Жаңа файл жасау\ncreate-new-folder = Жаңа бума жасау\nfile-name = Файл аты\nfolder-name = Бума аты\nfile-already-exists = Ондай аты бар файл бұрыннан бар\nfolder-already-exists = Ондай аты бар бума бұрыннан бар\nname-hidden = \".\" таңбасынан басталатын атаулар жасырын болады\nname-invalid = Аты \"{ $filename }\" болуы мүмкін емес\nname-no-slashes = Атауда қиғаш сызықтар болмауы тиіс\ncancel = Бас тарту\ncreate = Жасау\nopen = Ашу\nopen-file = Файлды ашу\nopen-folder = Буманы ашу\nopen-in-new-tab = Жаңа бетте ашу\nopen-in-new-window = Жаңа терезеде ашу\nopen-item-location = Нысанның орнын ашу\nopen-multiple-files = Бірнеше файлды ашу\nopen-multiple-folders = Бірнеше буманы ашу\nsave = Сақтау\nsave-file = Файлды сақтау\nopen-with-title = \"{ $name }\" қалай ашқыңыз келеді?\nbrowse-store = { $store } шолу\nother-apps = Басқа қолданбалар\nrelated-apps = Қатысты қолданбалар\nselected-items = Таңдалған { $items } нысан\npermanently-delete-question = Біржола өшіру керек пе?\ndelete = Өшіру\npermanently-delete-warning = { $target } біржола өшіріледі. Бұл әрекетті болдырмау мүмкін емес.\nrename-file = Файлдың атын өзгерту\nrename-folder = Буманың атын өзгерту\nreplace = Алмастыру\nreplace-title = Бұл жерде \"{ $filename }\" бұрыннан бар\nreplace-warning = Оны сақталып жатқан файлмен алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны қайта жазылады.\nreplace-warning-operation = Оны алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны үстінен жазылады.\noriginal-file = Түпнұсқа файл\nreplace-with = Келесімен алмастыру\napply-to-all = Барлығына іске асыру\nkeep-both = Екеуін де қалдыру\nskip = Өткізіп жіберу\nset-executable-and-launch = Орындалатын файл ретінде орнату және жөнелту\nset-executable-and-launch-description = \"{ $name }\" нысанын орындалатын файл ретінде орнатып, оны жөнелтуді қалайсыз ба?\nset-and-launch = Орнату және жөнелту\nopen-with = Көмегімен ашу\nowner = Иесі\ngroup = Топ\nother = Басқа\nnone = Ештеңе\nexecute-only = Тек орындау\nwrite-only = Тек жазу\nwrite-execute = Жазу және орындау\nread-only = Тек оқу\nread-execute = Оқу және орындау\nread-write = Оқу және жазу\nread-write-execute = Оқу, жазу және орындау\nfavorite-path-error = Буманы ашу қатесі\nfavorite-path-error-description =\n    \"{ $path }\" ашу мүмкін емес\n    \"{ $path }\" жоқ болуы мүмкін немесе оны ашуға құқығыңыз жоқ\n\n    Оны бүйірлік панельден өшіруді қалайсыз ба?\nremove = Өшіру\nkeep = Қалдыру\nrepository = Репозиторий\nsupport = Қолдау\nadd-network-drive = Желілік дискіні қосу\nconnect = Байланысу\nconnect-anonymously = Анонимді түрде байланысу\nconnecting = Байланысуда...\ndomain = Домен\nenter-server-address = Сервер адресін енгізіңіз\nnetwork-drive-description =\n    Сервер адрестері хаттама префиксі мен адрестен тұрады.\n    Мысалдар: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Қолжетімді хаттамалар,Префикс\n    AppleTalk,afp://\n    Файлды тасымалдау хаттамасы,ftp:// немесе ftps://\n    Желілік файлдық жүйе,nfs://\n    Сервер хабарламаларының блогы,smb://\n    SSH файлды тасымалдау хаттамасы,sftp:// немесе ssh://\n    WebDAV,dav:// немесе davs://\nnetwork-drive-error = Желілік дискіге қол жеткізу мүмкін емес\npassword = Пароль\nremember-password = Парольді есте сақтау\ntry-again = Қайтадан көру\nusername = Пайдаланушы аты\ncancelled = Бас тартылды\nedit-history = Тарихты түзету\nhistory = Тарихы\nno-history = Тарихта ешқандай элемент жоқ.\npending = Күтілуде\nprogress = { $percent } %\nprogress-cancelled = { $percent }%, бас тартылды\nprogress-failed = { $percent }%, сәтсіз аяқталды\nprogress-paused = { $percent }%, аялдатылды\nfailed = Сәтсіз аяқталды\ncomplete = Аяқталды\ncompressing =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне сығу ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне сығылды\ncopy_noun = Көшіріп алу\ncreating = \"{ $parent }\" ішінде \"{ $name }\" жасау\ncreated = \"{ $parent }\" ішінде \"{ $name }\" жасалды\ncopying =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне көшіру ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне көшірілді\ndeleting =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } { trash } ішінен өшіру ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } { trash } ішінен өшірілді\nemptying-trash = { trash } тазартылуда ({ $progress })...\nemptied-trash = { trash } тазартылды\nextracting =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне тарқату ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне тарқатылды\nsetting-executable-and-launching = \"{ $name }\" орындалатын файл ретінде орнату және іске қосу\nset-executable-and-launched = \"{ $name }\" орындалатын файл ретінде орнатылды және іске қосылды\nsetting-permissions = \"{ $name }\" үшін рұқсаттарды { $mode } мәніне орнату\nset-permissions = \"{ $name }\" үшін рұқсаттар { $mode } мәніне орнатылды\nmoving =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне жылжыту ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } \"{ $from }\" ішінен \"{ $to }\" ішіне жылжытылды\npermanently-deleting =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } біржола өшіру\npermanently-deleted =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } біржола өшірілді\nremoving-from-recents =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } { recents } тізімінен өшіру\nremoved-from-recents =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } { recents } тізімінен өшірілді\nrenaming = \"{ $from }\" атын \"{ $to }\" деп өзгерту\nrenamed = \"{ $from }\" аты \"{ $to }\" деп өзгертілді\nrestoring =\n    { $items } { $items ->\n        [one] нәрсені\n       *[other] нәрсені\n    } { trash } ішінен қалпына келтіру ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] нәрсе\n       *[other] нәрсе\n    } { trash } ішінен қалпына келтірілді\nunknown-folder = белгісіз бума\nmenu-open-with = Көмегімен ашу...\ndefault-app = { $name } (әдепкі)\nshow-details = Мәліметтерді көрсету\ntype = Түрі: { $mime }\nitems = Элементтер: { $items }\nitem-size = Өлшемі: { $size }\nitem-created = Жасалған: { $created }\nitem-modified = Өзгертілген: { $modified }\nitem-accessed = Қол жеткізілген: { $accessed }\ncalculating = Есептеу...\nsettings = Баптаулар\nsingle-click = Ашу үшін бір рет шерту\nappearance = Сыртқы түрі\ntheme = Тақырып\nmatch-desktop = Жұмыс үстеліне сәйкес келу\ndark = Күңгірт\nlight = Ашық\ntype-to-search = Іздеу үшін теру\ntype-to-search-recursive = Ағымдағы бума мен барлық ішкі бумаларды іздейді\ntype-to-search-enter-path = Бумаға немесе файлға жолды енгізеді\ntype-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды\nadd-to-sidebar = Бүйірлік панельге қосу\ncompress = Сығу...\ndelete-permanently = Біржолата өшіру\neject = Шығару\nextract-here = Тарқату\nnew-file = Жаңа файл...\nnew-folder = Жаңа бума...\nopen-in-terminal = Терминалда ашу\nmove-to-trash = Қоқыс жәшігіне тастау\nrestore-from-trash = Қоқыс жәшігінен қалпына келтіру\nremove-from-sidebar = Бүйірлік панельден өшіру\nsort-by-name = Аты бойынша сұрыптау\nsort-by-modified = Өзгертілген уақыты бойынша сұрыптау\nsort-by-size = Өлшемі бойынша сұрыптау\nsort-by-trashed = Өшірілген уақыты бойынша сұрыптау\nremove-from-recents = Соңғылардан өшіру\nchange-wallpaper = Тұсқағазды өзгерту...\ndesktop-appearance = Жұмыс үстелінің сыртқы түрі...\ndisplay-settings = Көрсету баптаулары...\nfile = Файл\nnew-tab = Жаңа бет\nnew-window = Жаңа терезе\nreload-folder = Буманы қайта жүктеу\nrename = Атын өзгерту...\nclose-tab = Бетті жабу\nquit = Шығу\nedit = Түзету\ncut = Қиып алу\ncopy = Көшіру\npaste = Кірістіру\nselect-all = Барлығын таңдау\nzoom-in = Үлкейту\ndefault-size = Әдепкі өлшем\nzoom-out = Кішірейту\nview = Көрініс\ngrid-view = Тор көрінісі\nlist-view = Тізім көрінісі\nshow-hidden-files = Жасырын файлдарды көрсету\nlist-directories-first = Алдымен бумаларды тізімдеу\ngallery-preview = Галереяны алдын ала қарау\nmenu-settings = Баптаулар...\nmenu-about = COSMIC файлдар туралы...\nsort = Сұрыптау\nsort-a-z = А-Я\nsort-z-a = Я-А\nsort-newest-first = Алдымен жаңалары\nsort-oldest-first = Алдымен ескілері\nsort-smallest-to-largest = Кішісінен үлкеніне\nsort-largest-to-smallest = Үлкенінен кішісіне\npasted-image = Кірістірілген сурет\npasted-text = Кірістірілген мәтін\npasted-video = Кірістірілген видео\ncopy-to-title = Көшіру мақсатын таңдаңыз\ncopy-to-button-label = Көшіріп алу\nmove-to-title = Жылжыту мақсатын таңдаңыз\nmove-to-button-label = Жылжыту\ncopy-to = Қайда көшіріп алу...\nmove-to = Қайда жылжыту...\ncomment = COSMIC жұмыс үстелі үшін файлдар басқарушысы\nkeywords = Folder;Manager;Бума;Басқарушы;\nshow-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы\nclear-recents-history = Жуырдағылар тарихын өшіру\ncopy-path = Орналасқан жолын көшіру\nmixed = Аралас\n"
  },
  {
    "path": "i18n/kmr/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/kn/cosmic_files.ftl",
    "content": "cosmic-files = ಕಾಸ್ಮಿಕ್ ಫೈಲ್ಸ್\nempty-folder = ಖಾಲಿ ಫೋಲ್ಡರ್\nempty-folder-hidden = ಖಾಲಿ ಫೋಲ್ಡರ್ (ಗೋಚರವಾಗದ ಐಟಂಗಳನ್ನು ಹೊಂದಿದೆ)\nno-results = ಯಾವುದೇ ಫಲಿತಾಂಶಗಳು ಇಲ್ಲ\nfilesystem = ಕಡತವ್ಯವಸ್ಥೆ\nhome = ಮನೆ\nnetworks = ನೆಟ್ವರ್ಕ್‌ಗಳು\nnotification-in-progress = ಫೈಲ್ ಕಾರ್ಯಾಚರಣೆಗಳು ಪ್ರಗತಿಯಲ್ಲಿದೆ.\ntrash = ಕಸ\nrecents = ಇತ್ತೀಚಿನ\nundo = ಹಿಂತಿರುಗಿಸು\ntoday = ಇಂದು\n# Desktop view options\ndesktop-view-options = ಡೆಸ್ಕ್‌ಟಾಪ್ ವೀಕ್ಷಣೆಯ ಆಯ್ಕೆಗಳು...\nshow-on-desktop = ಡೆಸ್ಕ್‌ಟಾಪ್‌ನಲ್ಲಿ ತೋರಿಸಿ\ndesktop-folder-content = ಡೆಸ್ಕ್‌ಟಾಪ್ ಫೋಲ್ಡರ್ ವಿಷಯ\nmounted-drives = ಮೌಂಟ್ ಮಾಡಿರುವ ಡ್ರೈವ್‌ಗಳು\ntrash-folder-icon = ಕಸದ ಫೋಲ್ಡರ್ ಐಕಾನ್\nicon-size-and-spacing = ಐಕಾನ್ ಗಾತ್ರ ಮತ್ತು ಅಂತರ\nicon-size = ಐಕಾನ್ ಗಾತ್ರ\n# List view\nname = ಹೆಸರು\nmodified = ಮಾರ್ಪಡಿಸಿದ ದಿನಾಂಕ\ntrashed-on = ಕಸದಲ್ಲಿ ಹಾಕಿದ ದಿನ\nsize = ಗಾತ್ರ\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = ಆರ್ಕೈವ್ ರಚಿಸಿ\n\n## Empty Trash Dialog\n\nempty-trash = ಕಸ ಖಾಲಿ ಮಾಡಿ\nempty-trash-warning = ಕಸದಲ್ಲಿನ ಎಲ್ಲಾ ಐಟಂಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಿಸಲು ನೀವು ನಿಜವಾಗಿಯೂ ಬಯಸುತ್ತೀರಾ?\n\n## New File/Folder Dialog\n\ncreate-new-file = ಹೊಸ ಫೈಲ್ ರಚಿಸಿ\ncreate-new-folder = ಹೊಸ ಫೋಲ್ಡರ್ ರಚಿಸಿ\nfile-name = ಫೈಲ್ ಹೆಸರು\nfolder-name = ಫೋಲ್ಡರ್ ಹೆಸರು\nfile-already-exists = ಅದೇ ಹೆಸರಿನ ಫೈಲ್ ಈಗಾಗಲೇ ಇದೆ.\nfolder-already-exists = ಅದೇ ಹೆಸರಿನ ಫೋಲ್ಡರ್ ಈಗಾಗಲೇ ಇದೆ.\nname-hidden = \".\" ಅಕ್ಷರಗಳಿಂದ ಪ್ರಾರಂಭವಾಗುವ ಹೆಸರುಗಳು ಮರೆಮಾಡಲ್ಪಡುತ್ತವೆ.\nname-invalid = ಹೆಸರು \"{ $filename }\" ಆಗಿರಲು ಸಾಧ್ಯವಿಲ್ಲ.\nname-no-slashes = ಹೆಸರಿನಲ್ಲಿ ಸ್ಲ್ಯಾಶ್‌ಗಳು ಇರಲು ಸಾಧ್ಯವಿಲ್ಲ.\n\n## Open/Save Dialog\n\ncancel = ರದ್ದು\ncreate = ರಚಿಸಿ\nopen = ತೆರೆಯಿರಿ\nopen-file = ಫೈಲ್ ತೆರೆಯಿರಿ\nopen-folder = ಫೋಲ್ಡರ್ ತೆರೆಯಿರಿ\nopen-in-new-tab = ಹೊಸ ಟ್ಯಾಬ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ\nopen-in-new-window = ಹೊಸ ವಿಂಡೋದಲ್ಲಿ ತೆರೆಯಿರಿ\nopen-item-location = ಐಟಂ ಸ್ಥಳ ತೆರೆಯಿರಿ\nopen-multiple-files = ಬಹು ಫೈಲ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ\nopen-multiple-folders = ಬಹು ಫೋಲ್ಡರ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ\nsave = ಉಳಿಸಿ\nsave-file = ಫೈಲ್ ಉಳಿಸಿ\n\n## Open With Dialog\n\nopen-with-title = \"{ $name }\" ಅನ್ನು ಹೇಗೆ ತೆರೆಯಲು ನೀವು ಬಯಸುತ್ತೀರಿ?\nbrowse-store = { $store } ಅಲ್ಲಿ ಹುಡುಕಿ\n\n## Rename Dialog\n\nrename-file = ಫೈಲ್ ಹೆಸರು ಬದಲಿಸಿ\nrename-folder = ಫೋಲ್ಡರ್ ಹೆಸರು ಬದಲಿಸಿ\n\n## Replace Dialog\n\nreplace = ಬದಲಾಯಿಸಿ\nreplace-title = { $filename } ಈಗಾಗಲೇ ಈ ಸ್ಥಳದಲ್ಲಿದೆ.\nreplace-warning = ಉಳಿಸಿರುವ ಫೈಲ್ ಅನ್ನು ಬದಲಾಯಿಸುತ್ತೀರಾ? ಬದಲಾಯಿಸಿದರೆ, ಉಳಿಸಿರುವ ಫೈಲ್ ಮೀರಿಸಿ ಬರೆಯಲಾಗುತ್ತದೆ.\nreplace-warning-operation = ನೀವು ಅದನ್ನು ಬದಲಿಸಲು ಬಯಸುತ್ತೀರಾ? ಬದಲಾಯಿಸಿದರೆ, ಉಳಿಸಿರುವ ಫೈಲ್ ಮೀರಿಸಿ ಬರೆಯಲಾಗುತ್ತದೆ.\noriginal-file = ಮೂಲ ಫೈಲ್\nreplace-with = ಇದರೊಂದಿಗೆ ಬದಲಾಯಿಸಿ\napply-to-all = ಎಲ್ಲರಿಗೂ ಅನ್ವಯಿಸಿ\nkeep-both = ಎರಡನ್ನೂ ಇಟ್ಟುಕೊಳ್ಳಿ\nskip = ಬಿಟ್ಟುಬಿಡಿ\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಿ\nset-executable-and-launch-description = ನೀವು \"{ $name }\" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಬೇಕೆಂದು ಬಯಸುವಿರಾ?\nset-and-launch = ಸೆಟ್ ಮಾಡಿ ಮತ್ತು ಪ್ರಾರಂಭಿಸಿ\n\n## Metadata Dialog\n\nowner = ಮಾಲೀಕ\ngroup = ಗುಂಪು\nother = ಇತರ\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = ನೆಟ್ವರ್ಕ್ ಡ್ರೈವ್ ಸೇರಿಸಿ\nconnect = ಸಂಪರ್ಕಿಸಿ\nconnect-anonymously = ಅನಾಮಿಕವಾಗಿ ಸಂಪರ್ಕಿಸಿ\nconnecting = ಸಂಪರ್ಕಿಸುತ್ತಿದೆ...\ndomain = ಡೊಮೇನ್\nenter-server-address = ಸರ್ವರ್ ವಿಳಾಸವನ್ನು ನಮೂದಿಸಿ\nnetwork-drive-description =\n    ಸರ್ವರ್ ವಿಳಾಸಗಳು ಪ್ರೋಟೋಕಾಲ್ ಪೂರ್ವಪ್ರತ್ಯಯ ಮತ್ತು ವಿಳಾಸವನ್ನು ಒಳಗೊಂಡಿರುತ್ತವೆ.\n    ಉದಾಹರಣೆಗಳು: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    ಲಭ್ಯವಿರುವ ಪ್ರೋಟೋಕಾಲ್‌ಗಳು,ಪೂರ್ವಪ್ರತ್ಯಯ\n    AppleTalk,afp://\n    ಫೈಲ್ ವರ್ಗಾವಣೆ ಪ್ರೋಟೋಕಾಲ್,ftp:// ಅಥವಾ ftps://\n    ನೆಟ್ವರ್ಕ್ ಫೈಲ್ ಸಿಸ್ಟಮ್,nfs://\n    ಸರ್ವರ್ ಸಂದೇಶ ಬ್ಲಾಕ್,smb://\n    SSH ಫೈಲ್ ವರ್ಗಾವಣೆ ಪ್ರೋಟೋಕಾಲ್,sftp:// ಅಥವಾ ssh://\n    ವೆಬ್ಡಾವ್,dav:// ಅಥವಾ davs://\nnetwork-drive-error = ನೆಟ್ವರ್ಕ್ ಡ್ರೈವ್‌ಗೆ ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ\npassword = ಪಾಸ್ವರ್ಡ್\nremember-password = ಪಾಸ್ವರ್ಡ್ ನೆನಪಿಡಿ\ntry-again = ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ\nusername = ಬಳಕೆಹೆಸರು\n\n## Operations\n\nedit-history = ಸಂಪಾದನೆ ಇತಿಹಾಸ\nhistory = ಇತಿಹಾಸ\nno-history = ಇತಿಹಾಸದಲ್ಲಿ ಐಟಂಗಳಿಲ್ಲ\npending = ಬಾಕಿ\nfailed = ವಿಫಲವಾಗಿದೆ\ncomplete = ಪೂರ್ಣವಾಗಿದೆ\ncompressing =\n    ಸಂಕುಚಿತ ಮಾಡಲಾಗುತ್ತಿದೆ { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to }\ncompressed =\n    ಸಂಕುಚಿತ ಮಾಡಲಾಯಿತು { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to }\ncopy_noun = ನಕಲು\ncreating = { $parent } ನಲ್ಲಿ { $name } ರಚಿಸಲಾಗುತ್ತಿದೆ\ncreated = { $parent } ನಲ್ಲಿ { $name } ರಚಿಸಲಾಯಿತು\ncopying =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ನಕಲು ಮಾಡಲಾಗುತ್ತಿದೆ\ncopied =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ನಕಲು ಮಾಡಲಾಯಿತು\nemptying-trash = ಕಸ ಖಾಲಿ ಮಾಡಲಾಗುತ್ತಿದೆ\nemptied-trash = ಕಸ ಖಾಲಿ ಮಾಡಲಾಯಿತು\nextracting =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ಹೊರತೆಗೆಯಲಾಗುತ್ತಿದೆ\nextracted =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ಹೊರತೆಗೆಯಲಾಯಿತು\nsetting-executable-and-launching = \"{ $name }\" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಲಾಗುತ್ತಿದೆ ಮತ್ತು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ\nset-executable-and-launched = \"{ $name }\" ಅನ್ನು ಕಾರ್ಯನಿರ್ವಹಿಸುವಂತೆ ಸೆಟ್ ಮಾಡಲಾಗಿದೆ ಮತ್ತು ಪ್ರಾರಂಭಿಸಲಾಗಿದೆ\nmoving =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ಸ್ಥಳಾಂತರಿಸಲಾಗುತ್ತಿದೆ\nmoved =\n    { $items } { $items ->\n        [one] ಐಟಂ\n       *[other] ಐಟಂಗಳು\n    } ನಿಂದ { $from } ಗೆ { $to } ಸ್ಥಳಾಂತರಿಸಲಾಯಿತು\nrenaming = { $from } ನಿಂದ { $to } ಗೆ ಹೆಸರು ಬದಲಾಯಿಸಲಾಗುತ್ತಿದೆ\nrenamed = { $from } ನಿಂದ { $to } ಗೆ ಹೆಸರು ಬದಲಾಯಿಸಲಾಯಿತು\nrestoring =\n    { $items } { $items ->\n        [one] ಐಟಂ    \n       *[other] ಐಟಂಗಳು\n    } ಅನ್ನು ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ\nrestored =\n    { $items } { $items ->\n        [one] ಐಟಂ    \n       *[other] ಐಟಂಗಳು\n    } ಅನ್ನು ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಲಾಯಿತು\nunknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್\n\n## Open with\n\nmenu-open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ\ndefault-app = { $name } (ಸ್ಥೂಲ)\n\n## Show details\n\nshow-details = ವಿವರಗಳನ್ನು ತೋರಿಸಿ\n\n## Settings\n\nsettings = ಸೆಟ್ಟಿಂಗ್‌ಗಳು\n\n### Appearance\n\nappearance = ನೋಟ\ntheme = ಥೀಮ್\nmatch-desktop = ಸಿಸ್ಟಮ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ ಅನುಗುಣವಾಗಿ\ndark = ಡಾರ್ಕ್\nlight = ಲೈಟ್\n# Context menu\nadd-to-sidebar = ಸೈಡ್‌ಬಾರ್‌ಗೆ ಸೇರಿಸಿ\ncompress = ಸಂಕ್ಷಿಪ್ತಗೊಳಿಸಿ\nextract-here = ಹೊರತೆಗೆಯಿರಿ\nnew-file = ಹೊಸ ಫೈಲ್...\nnew-folder = ಹೊಸ ಫೋಲ್ಡರ್...\nopen-in-terminal = ಟರ್ಮಿನಲ್‌ನಲ್ಲಿ ತೆರೆಯಿರಿ\nmove-to-trash = ಕಸಕ್ಕೆ ಸರಿಸಿ\nrestore-from-trash = ಕಸದಿಂದ ಮರುಸ್ಥಾಪಿಸಿ\nremove-from-sidebar = ಸೈಡ್‌ಬಾರ್‌ನಿಂದ ತೆಗೆಯಿರಿ\nsort-by-name = ಹೆಸರಿನಿಂದ ವಿಂಗಡಿಸಿ\nsort-by-modified = ತಿದ್ದುಪಡಿ ಮಾಡಿದ ದಿನಾಂಕದಿಂದ ವಿಂಗಡಿಸಿ\nsort-by-size = ಗಾತ್ರದಿಂದ ವಿಂಗಡಿಸಿ\nsort-by-trashed = ಅಳಿಸಿದ ದಿನಾಂಕದಿಂದ ವಿಂಗಡಿಸಿ\n\n## Desktop\n\nchange-wallpaper = ವಾಲ್‌ಪೇಪರ್ ಬದಲಾಯಿಸಿ...\ndesktop-appearance = ಡೆಸ್ಕ್‌ಟಾಪ್ ಕಾಣಿಕೆ...\ndisplay-settings = ಡಿಸ್ಪ್ಲೇ ಸೆಟ್ಟಿಂಗ್‌ಗಳು...\n\n# Menu\n\n\n## File\n\nfile = ಫೈಲ್\nnew-tab = ಹೊಸ ಟ್ಯಾಬ್\nnew-window = ಹೊಸ ವಿಂಡೋ\nrename = ಮರುಹೆಸರು...\nclose-tab = ಟ್ಯಾಬ್‌ನ್ನು ಮುಚ್ಚಿ\nquit = ಮುಗಿಸಿ\n\n## Edit\n\nedit = ಸಂಪಾದಿಸಿ\ncut = ಕತ್ತರಿಸಿ\ncopy = ನಕಲಿಸಿ\npaste = ಅಂಟಿಸಿ\nselect-all = ಎಲ್ಲವನ್ನು ಆಯ್ಕೆಮಾಡಿ\n\n## View\n\nzoom-in = ಜೂಮ್ ಇನ್\ndefault-size = ಸ್ಥೂಲ ಗಾತ್ರ\nzoom-out = ಜೂಮ್ ಔಟ್\nview = ದೃಶ್ಯ\ngrid-view = ಗ್ರೀಡ್ ವೀಕ್ಷಣೆ\nlist-view = ಪಟ್ಟಿಯ ವೀಕ್ಷಣೆ\nshow-hidden-files = ಮರೆಮಾಡಿದ ಫೈಲ್‌ಗಳನ್ನು ತೋರಿಸಿ\nlist-directories-first = ಡೈರೆಕ್ಟರಿಗಳನ್ನು ಮೊದಲು ತೋರಿಸಿ\nmenu-settings = ಸೆಟ್ಟಿಂಗ್‌ಗಳು...\nmenu-about = COSMIC ಫೈಲ್ ಬಗ್ಗೆ...\n\n## Sort\n\nsort = ವಿಂಗಡಿಸಿ\nsort-a-z = ಅ-ಆ ಕ್ರಮದಲ್ಲಿ ವಿಂಗಡಿಸಿ\nsort-z-a = ಆ-ಅ ಕ್ರಮದಲ್ಲಿ ವಿಂಗಡಿಸಿ\nsort-newest-first = ಹೊಸದರಿಂದ ಹಳೆಯದು\nsort-oldest-first = ಹಳೆಯದರಿಂದ ಹೊಸದು\nsort-smallest-to-largest = ಚಿಕ್ಕದರಿಂದ ದೊಡ್ಡದು\nsort-largest-to-smallest = ದೊಡ್ಡದರಿಂದ ಚಿಕ್ಕದು\n"
  },
  {
    "path": "i18n/ko/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC 파일\nempty-folder = 빈 폴더\nempty-folder-hidden = 빈 폴더 (숨겨진 항목 있음)\nfilesystem = 파일 시스템\nhome = 홈\ntrash = 휴지통\n# New File/Folder Dialog\ncreate-new-file = 새 파일 만들기\ncreate-new-folder = 새 폴더 만들기\nfile-name = 파일 이름\nfolder-name = 폴더 이름\nfile-already-exists = 같은 이름의 파일이 이미 있습니다\nfolder-already-exists = 같은 이름의 폴더가 이미 있습니다\nname-hidden = \".\" 으로 시작하는 항목은 숨겨집니다\nname-invalid = \"{ $filename }\"은(는) 사용할 수 없는 이름입니다\nname-no-slashes = 이름에 슬래시(/)를 사용할 수 없습니다\n# Open/Save Dialog\ncancel = 취소\nopen = 열기\nopen-file = 파일 열기\nopen-folder = 폴더 열기\nopen-multiple-files = 여러 파일 열기\nopen-multiple-folders = 여러 폴더 열기\nsave = 저장\nsave-file = 파일 저장\n# Rename Dialog\nrename-file = 파일 이름 바꾸기\nrename-folder = 폴더 이름 바꾸기\n# Replace Dialog\nreplace = 대체\nreplace-title = \"{ $filename }\" 이(가) 이미 현재 위치에 있습니다\nreplace-warning = 저장하려는 파일로 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다.\n# List view\nname = 이름\nmodified = 수정된 날짜\nsize = 크기\n\n# Context Pages\n\n\n## About\n\n\n## Operations\n\npending = 진행 중\nfailed = 실패함\ncomplete = 완료\n\n## Open with\n\nmenu-open-with = 다른 앱으로 열기\ndefault-app = { $name } (기본)\n\n## Properties\n\n\n## Settings\n\nsettings = 설정\n\n### Appearance\n\nappearance = 외관\ntheme = 테마\nmatch-desktop = 데스크톱에 맞춤\ndark = 다크\nlight = 라이트\n# Context menu\nnew-file = 새 파일...\nnew-folder = 새 폴더...\nopen-in-terminal = 터미널에서 열기\nmove-to-trash = 휴지통으로 이동\nrestore-from-trash = 휴지통에서 복구\nsort-by-name = 파일명으로 정렬\nsort-by-modified = 수정된 날짜 순으로 정렬\nsort-by-size = 크기 순으로 정렬\n\n# Menu\n\n\n## File\n\nfile = 파일\nnew-tab = 새 탭\nnew-window = 새 창\nrename = 이름 바꾸기...\nclose-tab = 탭 닫기\nquit = 종료\n\n## Edit\n\nedit = 편집\ncut = 잘라내기\ncopy = 복사\npaste = 붙여넣기\nselect-all = 모두 선택\n\n## View\n\nview = 보기\ngrid-view = 그리드 보기\nlist-view = 목록 보기\nmenu-settings = 설정...\nmenu-about = COSMIC 파일 정보...\nconnect = 연결\nread-execute = 읽기 및 실행\nitem-modified = 마지막 수정 일자: { $modified }\ndismiss = 메시지 무시\ncopy_noun = 복사\nprogress = { $percent }%\nrelated-apps = 관련 앱\ncompress = 압축...\nnetwork-drive-error = 네트워크 드라이브에 접근할 수 없음\nicon-size-and-spacing = 아이콘 크기 및 간격\npassword = 암호\ntype-to-search-enter-path = 폴더 혹은 파일의 경로 입력\nemptying-trash = { trash } 비우는 중 ({ $progress })...\ntrashed-on = 버려짐\nremove = 제거\noriginal-file = 원본 파일\ncreate = 생성\ncreate-archive = 압축 생성\nread-write-execute = 읽기, 쓰기 및 실행\nother-apps = 다른 앱\nset-permissions = \"{ $name }\"의 권한을 { $mode }로 설정함\npause = 정지\ncalculating = 계산 중...\nkeep = 유지\nitem-size = 크기: { $size }\nconnecting = 연결 중...\nread-write = 읽기 및 쓰기\nnone = 없음\nitems = 항목: { $items }\nno-results = 결과 없음\ntype = 형식: { $mime }\nresume = 재개\nremember-password = 암호 저장\nusername = 사용자 이름\nshow-details = 세부 사항 표시\nextract-to = 다른 위치에 압축 해제...\nadd-network-drive = 네트워크 드라이브 추가\ndelete = 삭제\nrepository = 저장소\nreplace-warning-operation = 해당 항목을 대체할까요? 대체 시 내용을 덮어쓰게 됩니다.\nsupport = 지원\ntry-again = 다시 시도\neject = 꺼내기\nother = 기타\nopen-in-new-window = 새 창에서 열기\nread-only = 읽기 전용\nbrowse-store = { $store } 둘러보기\nenter-server-address = 서버 주소 입력\nconnect-anonymously = 익명으로 연결\ngroup = 그룹\napply-to-all = 모두 적용\nskip = 건너뛰기\nreplace-with = 대체할 파일\nrecents = 최근\nnetwork-drive-description =\n    서버 주소는 프로토콜 접두어와 주소를 포함해야 합니다.\n    예시: ssh://192.168.0.1, ftp://[2001:db8::1]\nsingle-click = 클릭 한 번으로 열기\nundo = 되돌리기\nsetting-permissions = \"{ $name }\"의 권한을 { $mode }로 설정 중\nowner = 소유자\ncreating = \"{ $parent }\"에 \"{ $name }\" 생성 중\nexecute-only = 실행 전용\nopen-item-location = 항목 위치 열기\ndetails = 세부 정보\nmounted-drives = 마운트된 드라이브\nmount-error = 드라이브에 접근할 수 없음\nextract-here = 압축 해제\nremoved-from-recents = { recents } 에서 { $items }개의 항목을 제거했습니다\nadd-to-sidebar = 사이드 바에 추가\nitem-created = 생성 일자: { $created }\ntype-to-search-recursive = 현재 폴더와 하위 폴더 탐색\nhistory = 기록\nprogress-paused = { $percent }%, 정지됨\ndesktop-view-options = 바탕화면 표시 설정...\nshow-on-desktop = 바탕화면에 표시\ncancelled = 취소됨\ndomain = 도메인\nedit-history = 기록 수정\nprogress-failed = { $percent }%, 실패함\nitem-accessed = 마지막 접근 일자: { $accessed }\nextract-to-title = 폴더로 압축 해제\nopen-with = 다음으로 열기\nkeep-both = 둘 다 유지\nicon-size = 아이콘 크기\nopen-with-title = \"{ $name }\"을(를) 어떻게 열까요?\nwrite-execute = 쓰기 및 실행\nextract-password-required = 암호 필요\ndesktop-folder-content = 바탕화면 폴더 내용\nno-history = 기록된 항목이 없습니다.\nemptied-trash = { trash } 비워짐\nprogress-cancelled = { $percent }%, 취소됨\nopen-in-new-tab = 새 탭에서 열기\nunknown-folder = 알 수 없는 폴더\ncreated = \"{ $parent }\"에 \"{ $name }\" 생성됨\ndelete-permanently = 완전히 삭제\nnetworks = 네트워크\nwrite-only = 쓰기 전용\ntoday = 오늘\npermanently-delete-warning = { $target } 이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다.\nempty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다\nempty-trash = 휴지통 비우기\nempty-trash-title = 휴지통을 비울까요?\ntype-to-search = 입력하여 검색\nnotification-in-progress = 파일 작업이 진행 중입니다\npermanently-delete-question = 완전히 삭제할까요?\nselected-items = { $items }개 항목 선택됨\nsort-newest-first = 새 항목 우선\nrenamed = \"{ $from }\" 에서 \"{ $to }\" 로 이름 변경됨\ndeleted = { trash } 에서 { $items }개의 항목을 제거했습니다\nreload-folder = 폴더 새로고침\nfavorite-path-error = 디렉터리를 여는 중 오류가 발생했습니다\nremove-from-sidebar = 사이드 바에서 제거\nrestoring = { trash } 에서 { $items }개의 항목을 복구 중 ({ $progress })...\ngallery-preview = 갤러리 미리보기\nsort-smallest-to-largest = 작은 항목부터 큰 항목\nzoom-in = 확대\nremoving-from-recents = { recents } 에서 { $items }개의 항목을 제거 중\nzoom-out = 축소\ncompressing = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 압축 중({ $progress })...\nsetting-executable-and-launching = \"{ $name }\"를 실행 가능으로 설정 및 실행 중\ndefault-size = 기본 크기\nextracted = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 압축 해제했습니다\npermanently-deleting = { $items }개의 항목을 영구적으로 제거 중\ncompressed = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 압축했습니다\ngrid-spacing = 그리드 간격\ncopying = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 복사 중({ $progress })...\nsort-oldest-first = 오래된 항목 우선\nsort-by-trashed = 삭제된 시간 순으로 정렬\ncopied = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 복사했습니다\nlist-directories-first = 폴더 우선 나열\nremove-from-recents = 최근 항목에서 제거\nmoving = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 이동 중 ({ $progress })...\nchange-wallpaper = 배경화면 변경...\ndeleting = { trash } 에서 { $items }개의 항목을 제거 중({ $progress })...\nset-executable-and-launched = \"{ $name }\"를 실행 가능으로 설정 및 실행됨\nsort-a-z = A-Z\nset-and-launch = 설정 후 실행\nset-executable-and-launch = 실행 가능으로 설정 후 실행\nrestored = { trash } 에서 { $items }개의 항목을 복구했습니다\nsort-z-a = Z-A\noperations-running-finished = { $running }개의 작업 진행 중 ({ $percent }%), { $finished } 완료됨...\nsort = 정렬\nshow-hidden-files = 숨긴 파일 표시\ntrash-folder-icon = 휴지통 아이콘\nextracting = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 압축 해제 중 ({ $progress })...\npermanently-deleted = { $items }개의 항목을 영구적으로 제거했습니다\nrenaming = \"{ $from }\" 에서 \"{ $to }\" 로 이름 변경 중\nset-executable-and-launch-description = \"{ $name }\"을 실행 가능으로 설정하고 실행할까요?\nsort-largest-to-smallest = 큰 항목부터 작은 항목\nmoved = \"{ $from }\"에서 \"{ $to }\"(으)로 { $items }개의 항목을 이동했습니다\ndisplay-settings = 화면 설정...\ndesktop-appearance = 데스크톱 외관...\nfavorite-path-error-description =\n    \"{ $path }\"을(를) 열 수 없습니다\n    \"{ $path }\"이(가) 존재하지 않거나 열기 권한이 없을 수 있습니다\n\n    사이드바에서 제거하시겠습니까?\noperations-running = { $running }개의 작업 진행 중 ({ $percent }%)...\nnetwork-drive-schemes =\n    지원 프로토콜,접두사(Prefix)\n    AppleTalk,afp://\n    파일 전송 프로토콜 (FTP),ftp:// 또는 ftps://\n    네트워크 파일 시스템 (NFS),nfs://\n    서버 메시지 블록 (SMB),smb://\n    SSH 파일 전송 프로토콜 (SFTP),sftp:// 또는 ssh://\n    WebDAV,dav:// 또는 davs://\ntype-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다\ncomment = COSMIC 데스크톱용 위한 파일 관리자\nkeywords = 폴더;관리자;\ncopy-to-button-label = 복사\nmove-to-button-label = 이동\nclear-recents-history = 최근 기록 비우기\ncopy-path = 복사 경로\n"
  },
  {
    "path": "i18n/li/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/lt/cosmic_files.ftl",
    "content": "progress = { $percent }%\ncosmic-files = Cosmic Files\nempty-folder = Tuščias aplankas\nempty-folder-hidden = Tuščias aplankas (turi paslėptų failų)\nno-results = Rezultatų nėra\nfilesystem = Failų sistema\nhome = Namai\nnetworks = Tinklai\nnotification-in-progress = Vyksta failų operacijos\ntrash = Šiukšlinė\nrecents = Neseniai naudoti\nundo = Anuliuoti\ntoday = Šiandien\ndesktop-view-options = Darbalaukio peržiūros parinktys...\nshow-on-desktop = Rodyti darbalaukyje\ndesktop-folder-content = Darbalaukio aplanko turinys\nmounted-drives = Prijungti kaupikliai\ntrash-folder-icon = Šiukšlinės aplanko piktograma\nicon-size-and-spacing = Piktogramos dydis ir tarpai\nicon-size = Piktogramos dydis\ngrid-spacing = Tinklelio tarpai\nname = Pavadinimas\nmodified = Modifikuota\ntrashed-on = Ištrinta\nsize = Dydis\ndetails = Išsami informacija\ndismiss = Atmesti pranešimą\noperations-running =\n    { $running } { $running ->\n        [one] vykdoma operaciją\n       *[other] vykdomos operacijos\n    } ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] vykdoma operacija\n       *[other] vykdomos operacijos\n    } ({ $percent }%), { $finished } baigtos...\npause = Pauzė\nresume = Tęsti\ncreate-archive = Sukurti archyvą\nextract-password-required = Reikalingas slaptažodis\nextract-to = Išskleistį į...\nextract-to-title = Išskleisti į aplanką\nempty-trash-title = Ištuštinti šiukšlinę?\nempty-trash-warning = Šiukšlinjė esantys elementai bus ištrinti negrįžtamai\nmount-error = Nepavyko pasiekti kaupyklės\ncreate-new-file = Sukurti naują failą\ncreate-new-folder = Sukurti naują aplanką\nfile-name = Failo pavadinimas\nfolder-name = Aplanko pavadinimas\nfile-already-exists = Failas su tokiu vardu jau egzistuoja\nempty-trash = Ištuštinti šiukšlinę\nfolder-already-exists = Aplankas su tokiu vardu jau egzistuoja\nname-hidden = Pavadinimai prasidedantys „.“ simboliu bus paslėpti\nname-invalid = „{ $filename }“ negalima naudoti kaip pavadinimo\nname-no-slashes = Pavadinime negali būti pasvyrųjų brūkšnių\ncancel = Atšaukti\ncreate = Sukurti\nopen = Atidaryti\nopen-file = Atidaryti failą\nopen-folder = Atidaryti aplanką\nopen-in-new-tab = Atidaryti naujame skirtuke\nopen-in-new-window = Atidaryti naujame lange\nopen-item-location = Atidaryti elemento vietą\nopen-multiple-files = Atidaryti keletą failų\nopen-multiple-folders = Atidaryti keletą aplankų\nsave = Išsaugoti\nsave-file = Išsaugoti failą\nopen-with-title = Kaip norite atidaryti „{ $name }“?\nbrowse-store = Naršyti { $store }\nother-apps = Kitos aplikacijos\nrelated-apps = Susijusios aplikacijos\nselected-items = Pažymėti { $items } elementai\npermanently-delete-question = Ištrinti visam laikui?\ndelete = Ištrinti\npermanently-delete-warning = { $target } bus ištrintas visam laikui. Šis veiksmas yra negrįžtamas.\nrename-file = Pervadinti failą\nrename-folder = Pervadinti aplanką\nreplace = Pakeisti\nreplace-title = „{ $filename }“ jau egzistuoja šioje vietoje\nreplace-warning = Ar norite pakeisti tai su tuo, kas yra įrašoma? Keičiant bus pakeistas turinys.\nreplace-warning-operation = Ar norite pakeisti tai? Pakeičiant bus keičiamas turinys.\noriginal-file = Originalus failas\nreplace-with = Pakeisti su\napply-to-all = Pritaikyti visiems\nkeep-both = Palikti abu\nskip = Praleisti\nset-executable-and-launch = Nustatyti kaip paleidžiamą ir paleisti\nset-executable-and-launch-description = Ar norite nustatyti „{ $name }“ kaip paleidžiamą ir paleisti iš karto?\nset-and-launch = Nustatyti ir paleisti\nopen-with = Atidaryti su\nowner = Savininkas\ngroup = Grupė\nother = Kita\nnone = Joks\nexecute-only = Tik paleidžiamas\nwrite-only = Tik įrašomas\nwrite-execute = Įrašyti ir paleisti\nread-only = Tik skaitomas\nread-execute = Skaitomas ir paleidžiamas\nread-write = Skaitomas ir įrašomas\nread-write-execute = Skaitomas, įrašomas ir paleidžiamas\nfavorite-path-error = Klaida atidarant aplanką\nfavorite-path-error-description =\n    Nepavyko atidaryt „{ $path }“\n    „{ $path }“ gali neegzistuoti arba neturi teisių atidaryti\n\n    Ar norėtumėte pašalinti tai iš šonjuostės?\nremove = Pašalinti\nkeep = Išlaikyti\nrepository = Saugykla\nsupport = Palaikymas\nadd-network-drive = Pridėti tinklo talpyklą\nconnect = Prijungti\nconnect-anonymously = Prijungti anonimiškai\nconnecting = Jungiamasi...\ndomain = Domenas\nenter-server-address = Įveskite serverio adresą\nnetwork-drive-description =\n    Serverio adresą sudaro protokolas ir adresas.\n    Pavyzdžiui: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Galimi protokolai, Prefiksas\n    AppleTalk, afp://\n    Duomenų Perdavimo Protokolas (File Transfer Protocol), ftp:// or ftps://\n    Tinklo Failų Sistema (Network File System), nfs://\n    Serverio Žinučių Blokas (Server Message Block), smb://\n    SSH Duomenų Perdavimo Protokolas, sftp:// or ssh://\n    WebDAV, dav:// or davs://\nnetwork-drive-error = Nepavyko pasiekti tinklo kaupyklos\npassword = Slaptažodis\nremember-password = Prisiminti slaptažodį\ntry-again = Pabandyti dar kartą\nusername = Naudotojo vardas\ncancelled = Atšaukta\nedit-history = Redaguoti istoriją\nhistory = Istorija\nno-history = Istorija tuščia.\npending = Laukiama patvirtinimo\nprogress-cancelled = { $percent }%, atšaukta\nprogress-failed = { $percent }%, nepavyko\nprogress-paused = { $percent }%, pristabdyta\nfailed = Nepavyko\ncomplete = Baigta\ncompressing =\n    Suspaudžiami { $items } { $items ->\n        [one] elementas\n       *[other] elementai\n    } iš „{ $from }“ į „{ $to }“ ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] Suspaustas elementas\n       *[other] Suspausti elementai\n    } iš „{ $from }“ to „{ $to }“\ncopy_noun = Kopijuoti\ncreating = Kuriamas „{ $name }“ „{ $parent }“ lokacijoje\ncreated = Sukurtas „{ $name }“ „{ $parent }“ lokacijoje\ncopying =\n    { $items } { $items ->\n        [one] Kopijuojamas elementas\n       *[other] Kopijuojami elementai\n    } iš „{ $from }“ į „{ $to }“ ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] Nukopijuotas elementas\n       *[other] Nukopijuoti elementai\n    } iš „{ $from }“ į „{ $to }“\ndeleting =\n    { $items } { $items ->\n        [one] Trinamas elementas\n       *[other] Trinami elementai\n    } iš { trash } { $progress }...\nemptying-trash = Tuštinama { trash } ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] Ištrintas elementas\n       *[other] Ištrinti elementai\n    } iš { trash }\nemptied-trash = Ištuštinta { trash }\nextracting =\n    { $items } { $items ->\n        [one] Išskleidžiamas elementas\n       *[other] Išskleidžiami elementai\n    } iš „{ $from }“ į „{ $to }“ { $progress }...\nextracted =\n    { $items } { $items ->\n        [one] Iškleistas elementas\n       *[other] Iškleisti elementai\n    } iš „{ $from }“ į „{ $to }“\nsetting-executable-and-launching = Nustatomas „{ $name }“, kaip vykdomas, ir paleidžiamas\nset-executable-and-launched = Nustatyti „{ $name }“, kaip vykdomą, ir paleisti\nsetting-permissions = Nustatomi leidimai { $mode } \"{ $name }\"\nset-permissions = Nustatyti { $mode } leidimus „{ $name }“\nmoving =\n    { $items } { $items ->\n        [one] Perkeliamas elementas\n       *[other] Perkeliami elementai\n    } iš „{ $from }“ į „{ $to }“ { $progress }...\nmoved =\n    { $items } { $items ->\n        [one] Perkeltas elementas\n       *[other] Perkelti elementai\n    } iš „{ $from }“ į „{ $to }“\npermanently-deleting =\n    { $items } { $items ->\n        [one] Visam laikui ištrinamas elementas\n       *[other] Visam laikui ištrinami elementai\n    }\npermanently-deleted =\n    { $items } { $items ->\n        [one] Visam laikui ištrintas elementas\n       *[other] Visam laikui ištrinti elementai\n    }\nremoving-from-recents =\n    { $items } { $items ->\n        [one] Pašalinamas elementas\n       *[other] Pašalinami elementi\n    } iš { recents }\nremoved-from-recents =\n    { $items } { $items ->\n        [one] Pašalintas elementas\n       *[other] Pašalinti elementai\n    } iš { recents }\nrenaming = Pervadinamas „{ $from }“ į „{ $to }“\nrenamed = „{ $from }“ pervadintas į „{ $to }“\nrestoring =\n    { $items } { $items ->\n        [one] Atkuriamas elementas\n       *[other] Atkuriami elementai\n    } iš { trash } ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] Atkurtas elementas\n       *[other] Atkurti elementai\n    } iš { trash }\nunknown-folder = nežinomas aplankas\nmenu-open-with = Atidaryti su...\ndefault-app = { $name } (numatytas)\nshow-details = Rodyti išsamią informaciją\ntype = Tipas: { $mime }\nitems = Elementai: { $items }\nitem-size = Dydis: { $size }\nitem-created = Sukurtas: { $created }\nitem-modified = Modifikuota: { $modified }\nitem-accessed = Paskutinė prieiga: { $accessed }\ncalculating = Skaičiuojama...\nsettings = Nustatymai\nsingle-click = Vieno paspaudimo atidarymas\nappearance = Išvaizda\nmatch-desktop = Pagal darbalaukio temą\ntype-to-search = Norint ieškoti, pradėkite rašyti\ntype-to-search-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose\ntype-to-search-enter-path = Įvedamas aplanko ar failo kelias\nadd-to-sidebar = Pridėti į šonjuostę\ncompress = Suspausti...\ndelete-permanently = Ištrinti visam laikui\neject = Išstumti\nextract-here = Išskleisti\nnew-file = Naujas failas...\nnew-folder = Naujas aplankas...\nopen-in-terminal = Atidaryti terminale\nmove-to-trash = Perkelti į šiukšlinę\nrestore-from-trash = Atkurti iš šiukšlinės\nremove-from-sidebar = Pašalinti iš šonjuostės\nsort-by-name = Rikiuoti pagal pavadinimą\nsort-by-modified = Rikiuoti pagal modifikavimo laiką\nsort-by-size = Rikiuoti pagal dydį\nsort-by-trashed = Rikiuoti pagal ištrinimo laiką\nremove-from-recents = Pašalinti iš neseniai naudotų\nchange-wallpaper = Pakeisti darbalaukio foną...\ndesktop-appearance = Darbalaukio išvaizda...\ndisplay-settings = Vaizdo nustatymai...\nfile = Failas\nnew-tab = Naujas skirtukas\nnew-window = Naujas langas\nreload-folder = Perkrauti aplanką\nrename = Pervadinti...\nclose-tab = Uždaryti skirtuką\nquit = Išeiti\nedit = Redaguoti\ncut = Iškirpti\ncopy = Kopijuoti\npaste = Įklijuoti\nselect-all = Pažymėti viską\nzoom-in = Priartinti\ndefault-size = Numatytas dydis\nzoom-out = Nutolinti\nview = Rodymas\ngrid-view = Tinklelio išdėstymas\nlist-view = Sąrašo išdėstymas\nshow-hidden-files = Rodyti paslėptus failus\nlist-directories-first = Pirmiau pateikti aplankus\ngallery-preview = Galerijos peržiūra\nmenu-settings = Nustatymai...\nmenu-about = Apie COSMIC Files...\nsort = Rikiuoti\nsort-a-z = A-Ž\nsort-z-a = Ž-A\nsort-newest-first = Pirma naujausi\nsort-oldest-first = Pirma seniausi\nsort-smallest-to-largest = Nuo mažiausio iki didžiausio\nsort-largest-to-smallest = Nuo didžiausio iki mažiausio\ndark = Tamsus\nlight = Šviesus\ncomment = COSMIC desktop failų tvarkyklė\nkeywords = Aplankas;Tvarkyklė;\ncopy-to-title = Pasirinkti kopijavimo vietą\ncopy-to-button-label = Kopijuoti\nmove-to-title = Pasirinkti perkėlimo vietą\nmove-to-button-label = Perkelti\npasted-image = Įklijuotas Atvaizdas\npasted-text = Įklijuotas Tekstas\npasted-video = Įklijuotas Vaizdo įrašas\nshow-recents = Neseniai naudotų aplankas šonjuostėje\ntype-to-search-select = Parenkamas pirmas atitinkantis failas arba aplankas\nclear-recents-history = Išvalyti Neseniai naudotų istoriją\ncopy-to = Kopijuoti į...\nmove-to = Perkeltiį į...\ncopy-path = Kopijuoti kelią\n"
  },
  {
    "path": "i18n/ml/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/ms/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/nb-NO/cosmic_files.ftl",
    "content": "open-file = Åpne fil\nopen-folder = Åpne mappe\nsettings = Innstillinger\nappearance = Utseende\ntheme = Tema\ndark = Mørk\nlight = Lys\nfile = Fil\nquit = Avslutt\ncosmic-files = COSMIC Filer\nempty-folder = Tom mappe\ncancel = Avbryt\nempty-folder-hidden = Tom mappe (har skjulte filer)\nno-results = Ingen resultater\nsupport = Støtte\nfilesystem = Filsystem\nhome = Hjem\nnetworks = Nettverk\nnotification-in-progress = Filoperasjoner pågår.\ndelete = Slett\ntrash = Papirkurv\nrecents = Nylige\nconnect = Koble til\nundo = Angre\nmatch-desktop = Følg skrivebordet\nopen = Åpne\ntoday = I dag\nname = Navn\ndesktop-view-options = Visningsalternativer for skrivebord…\nshow-on-desktop = Vis på skrivebord\ndesktop-folder-content = Innhold i skrivebordsmappe\nmounted-drives = Monterte disker\ntrash-folder-icon = Papirkurvikon\npassword = Passord\nremove = Fjern\nusername = Brukernavn\nicon-size-and-spacing = Ikonstørrelse og avstand\nicon-size = Ikonstørrelse\ngrid-spacing = Rutenettavstand\nsize = Størrelse\ndetails = Detaljer\ndismiss = Avvis beskjed\noperations-running =\n    { $running } { $running ->\n        [one] operasjon\n       *[other] operasjoner\n    } pågår ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operasjon\n       *[other] operasjoner\n    } pågår ({ $percent }%), { $finished } ferdige...\npause = Pause\nsave = Lagre\nrepository = Kodelager\nmodified = Modifisert\ntrashed-on = Slettet\nresume = Fortsett\ncreate-archive = Opprett arkiv\nextract-password-required = Passord kreves\nextract-to = Pakk ut til...\nextract-to-title = Pakk ut i mappe\nnew-tab = Ny fane\nnew-window = Nytt vindu\nclose-tab = Lukk fane\nedit = Rediger\ncopy = Kopier\npaste = Lim inn\nselect-all = Velg alt\nview = Vis\nmenu-settings = Innstillinger...\nfolder-name = Mappenavn\nreplace = Erstatt\nfile-already-exists = En fil med det navnet finnes allerede.\nfolder-already-exists = En mappe med det navnet finnes allerede.\nname-hidden = Navn som begynner med \".\" vil være skjult.\nopen-in-new-window = Åpne i nytt vindu\nempty-trash = Tøm papirkurven\nempty-trash-warning = Er du sikker på at du vil slette alle filene i papirkurven permanent?\nmount-error = Kunne ikke koble til disken\ncreate-new-file = Lag ny fil\ncreate-new-folder = Lag ny mappe\nfile-name = Filnavn\nname-invalid = Navn kan ikke være \"{ $filename }\".\nname-no-slashes = Navn kan ikke inneholde skråstrek.\ncreate = Skape\nopen-in-new-tab = Åpne i ny fane\nopen-item-location = Åpne filplassering\nopen-multiple-files = Åpne flere filer\nopen-multiple-folders = Åpne flere mapper\nsave-file = Lagre fil\nopen-with-title = Hvordan vil du åpne \"{ $name }\"?\nbrowse-store = Bla i { $store }\nother-apps = Andre applikasjoner\nrelated-apps = Relaterte applikasjoner\nselected-items = de { $items } valgte objektene\npermanently-delete-question = Slett permanent\npermanently-delete-warning = Er du sukker på om du vil permanent slette { $target }? Dette kan ikke angres.\nrename-file = Endre filnavn\nrename-folder = Endre mappenavn\nreplace-title = \"{ $filename }\" finnes allerede på denne plasseringen.\nreplace-warning = Vil du bytte den ut med filen du lagrer? Dette vil overskrive innholdet.\nreplace-warning-operation = Vil du erstatte den? Dette vil overskrive innholdet.\noriginal-file = Orginalfil\nreplace-with = Erstatt med\napply-to-all = Bruk på alle\nkeep-both = Behold begge\nskip = Hopp over\nset-executable-and-launch = Gjør kjørbar og start\nset-executable-and-launch-description = Vil du gjøre «{ $name }» kjørbar og starte den?\nopen-with = Åpne med\nowner = Eier\ngroup = Gruppe\nother = Andre\nnone = Ingen\nexecute-only = Kun kjørbar\nwrite-only = Kun skrivbar\nwrite-execute = Kun skrive- og kjørbar\nread-only = Kun lesbar\nread-execute = Kun lese- og kjørbar\nread-write = Kun skrive- og lesbar\nread-write-execute = Lesbar, skrivbar og kjørbar\nfavorite-path-error = Feil ved åpning av mappen\nfavorite-path-error-description =\n    Kunne ikke åpne \"{ $path }\".\n    Den kan ikke eksistere eller så har du ikke tilgang til å åpne den.\n\n    Vil du fjerne den fra sidepanelet?\nkeep = Behold\nadd-network-drive = Legg til nettverksdisk\nconnect-anonymously = Koble til anonymt\nconnecting = Kobler til...\ncancelled = Avbrutt\npending = Venter\nfailed = Mislykkede\ncomplete = Ferdige\nzoom-in = Zoom inn\ndefault-size = Standard størrelse\nzoom-out = Zoom ut\ncut = Klipp ut\nset-and-launch = Still inn og start\ndomain = Domene\nenter-server-address = Angi serveradresse\nnetwork-drive-description =\n    Serveradresser inkluderer ett protokollprefiks og en addresse.\n    Examples: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Tilgjengelige protokoller, Prefiks\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// eller ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// eller ssh://\n    WebDAV,dav:// eller davs://\nnetwork-drive-error = Kunne ikke nå nettverksdisk\nremember-password = Husk passord\ntry-again = Prøv igjen\nedit-history = Rediger hisorikk\nhistory = Historikk\nno-history = Ingenting i historikken.\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, avbrutt\nprogress-failed = { $percent }%, mislykket\nprogress-paused = { $percent }%, satt på pause\ncompressing =\n    Komprimerer { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\ncompressed =\n    Komprimerete { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\ncopy_noun = Kopi\ncreating = Oppretter \"{ $name }\" i \"{ $parent }\"\ncreated = Opprettet \"{ $name }\" i \"{ $parent }\"\ncopying =\n    Kopierer { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\" ({ $progress })...\ncopied =\n    Kopierete { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra \"{ $from }\" til \"{ $to }\"\ndeleting =\n    Sletter { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } fra { trash } ({ $progress })...\ndeleted =\n    Slettet { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { trash }\nemptying-trash = Tømmer { trash } ({ $progress })...\nemptied-trash = Tømte { trash }\nextracting =\n    Pakker ut  { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra «{ $from }» til «{ $to }» ({ $progress })...\nextracted =\n    Pakka ut  { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra «{ $from }» til «{ $to }»\nsetting-executable-and-launching = Setter «{ $name }» som kjørbar og starter\nset-executable-and-launched = Sett «{ $name }» som kjørbar og startet\nsetting-permissions = Setter tillatelser for «{ $name }» til { $mode }\nset-permissions = Sett tillatelser for «{ $name }» til { $mode }\nmoving =\n    Flytter { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra «{ $from }» til «{ $to }» ({ $progress })...\nmoved =\n    Flytta { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra «{ $from }» til «{ $to }»\npermanently-deleting =\n    Sletter { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } permanent\npermanently-deleted =\n    Sletta { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } permanent\nremoving-from-recents =\n    Fjerner { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { recents }\nremoved-from-recents =\n    Fjerna { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { recents }\nrenaming = Bytter navn «{ $from }» til «{ $to }»\nrenamed = Byttet navn «{ $from }» til «{ $to }»\nrestoring =\n    Gjenopretter { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { trash } ({ $progress })...\nrestored =\n    Gjenopretta { $items } { $items ->\n        [one] objekt\n       *[other] objekter\n    } fra { trash }\nunknown-folder = ukjent mappe\nmenu-open-with = Åpne med…\ndefault-app = { $name } (standard)\nshow-details = Vis detaljer\ntype = Type: { $mime }\nitems = Objekter: { $items }\nitem-size = Størrelse: { $size }\nitem-created = Opprettet: { $created }\nitem-modified = Endret: { $modified }\nitem-accessed = Åpnet: { $accessed }\ncalculating = Beregner...\nsingle-click = Ett klikk for å åpne\ntype-to-search = Skriv for å Søke\ntype-to-search-recursive = Søker i den aktuelle mappen og alle undermapper\ntype-to-search-enter-path = Åpner plasseringen til mappen eller filen\nadd-to-sidebar = Legg til sidepanelet\ncompress = Komprimer\ndelete-permanently = Slett permanent\neject = Løs ut\nextract-here = Pakk ut\nnew-file = Ny fil…\nnew-folder = Ny mappe…\nopen-in-terminal = Åpne i terminal\nmove-to-trash = Flytt til papirkurven\nrestore-from-trash = Gjenoprett fra papirkurven\nremove-from-sidebar = Fjern fra sidepanelet\nsort-by-name = Sorter etter navn\nsort-by-modified = Sorter etter modifisert\nsort-by-size = Sorter etter størrelse\nsort-by-trashed = Sorter etter slettingsdato\nremove-from-recents = Fjern fra nylige\nchange-wallpaper = Bytt bakgrunnsbilde…\ndesktop-appearance = Skrivebordsutseende...\ndisplay-settings = Skjerminnstillinger…\nreload-folder = Last inn mappe på nytt\nrename = Gi nytt navn…\ngrid-view = Rutevisning\nlist-view = Rutenettvisning\nshow-hidden-files = Vis skjulte filer\nlist-directories-first = List mapper først\ngallery-preview = Galleriforhåndsvisning\nmenu-about = Om COSMIC Filer...\nsort = Sorter\nsort-a-z = A-Å\nsort-z-a = Å-A\nsort-newest-first = Nyeste først\nsort-oldest-first = Eldste først\nsort-smallest-to-largest = Minste til største\nsort-largest-to-smallest = Største til minste\n"
  },
  {
    "path": "i18n/nl/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Bestanden\nempty-folder = Lege map\nempty-folder-hidden = Lege map (met verborgen items)\nno-results = Geen resultaten gevonden\nfilesystem = Bestandssysteem\nhome = Persoonlijke map\nnetworks = Netwerken\nnotification-in-progress = Bestandsbewerkingen worden uitgevoerd\ntrash = Prullenbak\nrecents = Recente bestanden\nundo = Ongedaan maken\ntoday = Vandaag\n# Desktop view options\ndesktop-view-options = Opties voor bureaubladweergave…\nshow-on-desktop = Op bureaublad weergeven\ndesktop-folder-content = Bestanden in de Bureablad-map\nmounted-drives = Aangekoppelde schijven\ntrash-folder-icon = Pictogram van de Prullenbak-map\nicon-size-and-spacing = Pictogramgrootte en -afstand\nicon-size = Pictogramgrootte\ngrid-spacing = Rasterafstand\n# List view\nname = Naam\nmodified = Gewijzigd\ntrashed-on = Verwijderd op\nsize = Grootte\n# Progress footer\ndetails = Details\ndismiss = Bericht negeren\noperations-running =\n    { $running } { $running ->\n        [one] bewerking wordt\n       *[other] bewerkingen worden\n    } uitgevoerd ({ $percent }%) …\noperations-running-finished =\n    { $running } { $running ->\n        [one] bewerking wordt\n       *[other] bewerkingen worden\n    } uitgevoerd ({ $percent }%), { $finished } voltooid...\npause = Pauzeren\nresume = Hervatten\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Archief aanmaken\n\n## Extract Dialog\n\nextract-password-required = Wachtwoord vereist\nextract-to = Uitpakken naar…\nextract-to-title = Uitpakken naar map\n\n## Empty Trash Dialog\n\nempty-trash = Prullenbak leegmaken\nempty-trash-warning = Bestanden in de Prullenbak-map worden permanent verwijderd\n\n## Mount Error Dialog\n\nmount-error = Geen toegang tot schijf\n\n## New File/Folder Dialog\n\ncreate-new-file = Nieuw bestand aanmaken\ncreate-new-folder = Nieuwe map aanmaken\nfile-name = Bestandsnaam\nfolder-name = Mapnaam\nfile-already-exists = Er bestaat al een bestand met deze naam\nfolder-already-exists = Er bestaat al een map met deze naam\nname-hidden = Namen die met “.” beginnen worden verborgen\nname-invalid = Ongeldige naam “{ $filename }”\nname-no-slashes = Naam mag geen schuine strepen bevatten\n\n## Open/Save Dialog\n\ncancel = Annuleren\ncreate = Aanmaken\nopen = Openen\nopen-file = Bestand openen\nopen-folder = Map openen\nopen-in-new-tab = In nieuw tabblad openen\nopen-in-new-window = In nieuw venster openen\nopen-item-location = Bestandslocatie openen\nopen-multiple-files = Meerdere bestanden openen\nopen-multiple-folders = Meerdere mappen openen\nsave = Opslaan\nsave-file = Bestand opslaan\n\n## Open With Dialog\n\nopen-with-title = Hoe wilt u “{ $name }” openen?\nbrowse-store = { $store } verkennen\nother-apps = Andere toepassingen\nrelated-apps = Gerelateerde toepassingen\n\n## Permanently delete Dialog\n\nselected-items = De { $items } geselecteerde items\npermanently-delete-question = Permanent verwijderen?\ndelete = Verwijderen\npermanently-delete-warning = { $target } wordt permanent verwijderd. Dit kan niet ongedaan gemaakt worden.\n\n## Rename Dialog\n\nrename-file = Bestand hernoemen\nrename-folder = Map hernoemen\n\n## Replace Dialog\n\nreplace = Vervangen\nreplace-title = “{ $filename }” bestaat al op deze locatie\nreplace-warning = Wilt u het bestaande bestand vervangen? Als u het vervangt, wordt de inhoud ervan overschreven.\nreplace-warning-operation = Wilt u het vervangen? Dit kan niet ongedaan gemaakt worden.\noriginal-file = Oorspronkelijk bestand\nreplace-with = Vervangen door\napply-to-all = Op alles toepassen\nkeep-both = Beide behouden\nskip = Overslaan\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Als uitvoerbaar instellen en dan starten\nset-executable-and-launch-description = Wilt u “{ $name }” als uitvoerbaar instellen en dan starten?\nset-and-launch = Uitvoerbaar maken en starten\n\n## Metadata Dialog\n\nopen-with = Openen met\nowner = Eigenaar\ngroup = Groep\nother = Anderen\n\n### Mode 0\n\nnone = Geen\n\n### Mode 1 (unusual)\n\nexecute-only = Alleen uitvoeren\n\n### Mode 2 (unusual)\n\nwrite-only = Alleen schrijven\n\n### Mode 3 (unusual)\n\nwrite-execute = Schijven en uitvoeren\n\n### Mode 4\n\nread-only = Alleen lezen\n\n### Mode 5\n\nread-execute = Lezen en uitvoeren\n\n### Mode 6\n\nread-write = Lezen en schrijven\n\n### Mode 7\n\nread-write-execute = Lezen, schrijven en uitvoeren\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Fout bij het openen van de map\nfavorite-path-error-description =\n    Kon de map '{ $path }' niet openen.\n    De map bestaat mogelijk niet of u heeft geen toestemming om die te openen.\n\n    Wilt u de map uit de favorieten verwijderen?\nremove = Verwijderen\nkeep = Behouden\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Netwerkschijf toevoegen\nconnect = Verbinden\nconnect-anonymously = Anoniem verbinden\nconnecting = Verbinding maken…\ndomain = Domein\nenter-server-address = Serveradres invoeren\nnetwork-drive-description =\n    Serveradressen bestaan uit protocolvoorvoegsel en netwerkadres.\n    Voorbeelden: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Beschikbare protocollen,Voorvoegsel\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// of ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// of ssh://\n    WebDav,dav:// of davs://\nnetwork-drive-error = Geen toegang tot de netwerkschijf\npassword = Wachtwoord\nremember-password = Wachtwoord onthouden\ntry-again = Opnieuw proberen\nusername = Gebruikersnaam\n\n## Operations\n\ncancelled = Geannuleerd\nedit-history = Geschiedenis bewerken\nhistory = Geschiedenis\nno-history = Geen items in de geschiedenis.\npending = In afwachting\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, geannuleerd\nprogress-paused = { $percent }%, gepauzeerd\nfailed = Mislukt\ncomplete = Voltooid\ncompressing =\n    { $items }  { $items ->\n        [one] bestand\n       *[other] bestanden\n    } van '{ $from }' naar '{ $to }' comprimeren ({ $progress })...\ncompressed =\n    { $items }  { $items ->\n        [one] bestand\n       *[other] bestanden\n    } gecomprimeerd van '{ $from }' naar '{ $to }'\ncopy_noun = Kopie\ncreating = '{ $name }' in '{ $parent }' aanmaken\ncreated = '{ $name }' in '{ $parent }' aangemaakt\ncopying =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } van '{ $from }' naar '{ $to }' kopiëren ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } gekopieerd van '{ $from }' naar '{ $to }'\ndeleting =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } uit { trash } verwijderen ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } verwijderd uit { trash }\nemptying-trash = { trash } recyclen ({ $progress })...\nemptied-trash = { trash } gerecycled\nextracting =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } van '{ $from }' naar '{ $to }' uitpakken ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } uitgepakt van '{ $from }' naar '{ $to }'\nsetting-executable-and-launching = '{ $name }' uitvoerbaar maken en starten\nset-executable-and-launched = '{ $name }' uitvoerbaar gemaakt en gestart\nsetting-permissions = Rechten voor '{ $name }' wijzigen in '{ $mode }'\nset-permissions = Rechten voor '{ $name }' gewijzigd in '{ $mode }'\nmoving =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } van '{ $from }' naar '{ $to }' verplaatsen ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } verplaatst van '{ $from }' naar '{ $to }'\npermanently-deleting =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } premanent verwijderen\npermanently-deleted =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } permanent verwijderd\nrenaming = '{ $from }' als '{ $to }' hernoemen\nrenamed = '{ $from }' als '{ $to }' hernoemd\nrestoring =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } uit { trash } terugzetten ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] bestand\n       *[other] bestanden\n    } uit { trash } teruggezet\nunknown-folder = Onbekende map\n\n## Open with\n\nmenu-open-with = Openen met…\ndefault-app = { $name } (standaard)\n\n## Show details\n\nshow-details = Details weergeven\ntype = Type: { $mime }\nitems = Bestanden: { $items }\nitem-size = Grootte: { $size }\nitem-created = Aangemaakt op: { $created }\nitem-modified = Bewerkt op: { $modified }\nitem-accessed = Geopend op: { $accessed }\ncalculating = Wordt berekend…\n\n## Settings\n\nsettings = Instellingen\nsingle-click = Een keer klikken om te openen\n\n### Appearance\n\nappearance = Uiterlijk\ntheme = Thema\nmatch-desktop = Systeemstandaard\ndark = Donker\nlight = Licht\n\n### Type to Search\n\ntype-to-search = Typ om te zoeken\ntype-to-search-recursive = In deze map en alle onderliggende mappen zoeken\ntype-to-search-enter-path = Naar de bestandslocatie of -naam zoeken\n# Context menu\nadd-to-sidebar = Favoriet aan zijbalk toevoegen\ncompress = Comprimeren\ndelete-permanently = Permanent verwijderen\nextract-here = Uitpakken\nnew-file = Nieuw bestand…\nnew-folder = Nieuwe map…\nopen-in-terminal = In terminal openen\nmove-to-trash = Naar prullenbak verplaatsen\nrestore-from-trash = Uit prullenbak terugzetten\nremove-from-sidebar = Favoriet uit zijbalk verwijderen\nsort-by-name = Sorteren op naam\nsort-by-modified = Sorteren op laatst bewerkt\nsort-by-size = Sorteren op grootte\nsort-by-trashed = Sorteren op tijdstip van verwijderen\n\n## Desktop\n\nchange-wallpaper = Schermachtergrond wijzigen...\ndesktop-appearance = Uiterlijk van het bureaublad…\ndisplay-settings = Beeldschermbeheer...\n\n# Menu\n\n\n## File\n\nfile = Bestand\nnew-tab = Nieuw tabblad\nnew-window = Nieuw venster\nreload-folder = Opnieuw laden\nrename = Hernoemen…\nclose-tab = Tabblad sluiten\nquit = Sluiten\n\n## Edit\n\nedit = Bewerken\ncut = Knippen\ncopy = Kopiëren\npaste = Plakken\nselect-all = Alles selecteren\n\n## View\n\nzoom-in = Inzoomen\ndefault-size = Standaardgrootte\nzoom-out = Uitzoomen\nview = Beeld\ngrid-view = Rasterweergave\nlist-view = Lijstweergave\nshow-hidden-files = Verborgen bestanden tonen\nlist-directories-first = Mappen bovenaan weergeven\ngallery-preview = Galerijweergave\nmenu-settings = Instellingen…\nmenu-about = Over COSMIC Bestanden…\n\n## Sort\n\nsort = Sorteren\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Nieuwste bovenaan\nsort-oldest-first = Oudste bovenaan\nsort-smallest-to-largest = Van klein naar groot\nsort-largest-to-smallest = Van groot naar klein\nprogress-failed = { $percent }%, mislukt\neject = Uitwerpen\nsupport = Ondersteuning\nremoving-from-recents =\n    { $items } { $items ->\n        [one] item\n       *[other] items\n    } van { recents }\npasted-image = Geplakte afbeelding\npasted-text = Geplakte tekst\npasted-video = Geplakte video\nrepository = Broncode\nempty-trash-title = Prullenbak leegmaken?\nremoved-from-recents =\n    { $items } { $items ->\n        [one] item\n       *[other] items\n    } uit { recents } verwijderd\nremove-from-recents = Uit recente verwijderen\ntype-to-search-select = Dit selecteert het eerst overeenkomende bestand of map\ncomment = Bestandsbeheerder voor COSMIC desktop\ncopy-to-title = Kopieerbestemming aanwijzen\ncopy-to-button-label = Kopiëren\nmove-to-button-label = Verplaatsen\ncopy-to = Kopiëren naar…\nmove-to = Verplaatsen naar…\nkeywords = Bestand;Map;Document;Verkenner\nmove-to-title = Verplaatsbestemming aanwijzen\n"
  },
  {
    "path": "i18n/nn/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Filer\nopen-file = Opna fil\nempty-folder = Tom mappe\ncancel = Avbryt\npassword = Passord\nsettings = Innstillingar\nskip = Hopp over\nname = Namn\nsave = Lagra\ndelete = Slett\n"
  },
  {
    "path": "i18n/oc/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/pa/cosmic_files.ftl",
    "content": "empty-folder = ਖਾਲੀ ਫੋਲਡਰ\nempty-folder-hidden = ਖਾਲੀ ਫੋਲਡਰ (ਲੁਕਵੀਆਂ ਚੀਜ਼ਾਂ ਹਨ)\nno-results = ਕੋਈ ਨਤੀਜਾ ਨਹੀਂ ਲੱਭਿਆ\nfilesystem = ਫ਼ਾਇਲ-ਸਿਸਟਮ\nhome = ਮੁੱਖ ਸਫ਼ਾ\nnetworks = ਨੈੱਟਵਰਕ\nnotification-in-progress = ਫ਼ਾਇਲ ਕਾਰਵਾਈਆਂ ਜਾਰੀ ਹਨ\ntrash = ਰੱਦੀ\nrecents = ਸੱਜਰੇ\nundo = ਵਾਪਸ\ntoday = ਅੱਜ\ndesktop-view-options = ਡੈਸਕਟਾਪ ਝਲਕ ਲਈ ਚੋਣਾਂ...\nshow-on-desktop = ਡੈਸਕਟਾਪ ਉਤੇ ਵੇਖਾਓ\ndesktop-folder-content = ਡੈਸਕਟਾਪ ਫੋਲਡਰ ਸਮੱਗਰੀ\nmounted-drives = ਮਾਊਂਟ ਕੀਤੇ ਡਿਵਾਈਸ\ntrash-folder-icon = ਰੱਦੀ ਫੋਲਡਰ ਦਾ ਆਈਕਾਨ\nicon-size-and-spacing = ਆਈਕਾਨ ਦਾ ਆਕਾਰ ਅਤੇ ਫ਼ਾਸਲਾ\nicon-size = ਆਈਕਾਨ ਦਾ ਆਕਾਰ\ngrid-spacing = ਗਰਿੱਡ ਫ਼ਾਸਲਾ\nname = ਨਾਂ\nmodified = ਸੋਧ ਕੀਤੀ\ntrashed-on = ਰੱਦੀ 'ਚ ਭੇਜਿਆ\nsize = ਆਕਾਰ\ndetails = ਵੇਰਵੇ\ndismiss = ਸੁਨੇਹੇ ਨੂੰ ਖ਼ਾਰਜ ਕਰੋ\noperations-running =\n    { $running } { $running ->\n        [one] ਕਾਰਵਾਈ\n       *[other] ਕਾਰਵਾਈਆਂ\n    } ਜਾਰੀ ਹਨ ({ $percent }%)...\npause = ਵਿਰਾਮ\nresume = ਮੁੜ-ਚਾਲੂ\ncreate-archive = ਅਕਾਇਵ ਬਣਾਓ\nextract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ\nextract-to = ਖਿਲਾਰੋ...\nextract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ\nempty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ\nempty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ?\nempty-trash-warning = ਰੱਦੀ ਫੋਲਡਰ ਵਿਚਲੀਆਂ ਚੀਜ਼ਾਂ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾ ਦਿੱਤੀਆਂ ਜਾਣਗੀਆਂ\ncreate-new-file = ਨਵੀਂ ਫ਼ਾਇਲ ਬਣਾਓ\ncreate-new-folder = ਨਵਾਂ ਫੋਲਡਰ ਬਣਾਓ\nfile-name = ਫ਼ਾਇਲ ਦਾ ਨਾਂ\nfolder-name = ਫੋਲਡਰ ਦਾ ਨਾਂ\nfile-already-exists = ਉਸ ਨਾਂ ਨਾਲ ਫ਼ਾਇਲ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ\nfolder-already-exists = ਉਸ ਨਾਂ ਨਾਲ ਫੋਲਡਰ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ\nname-hidden = ਨਾਂ ਦੇ ਸ਼ੁਰੂ ਵਿੱਚ \".\" ਹੋਣ ਵਾਲੀ ਚੀਜ਼ ਲੁਕਵੀਂ ਹੋਵੇਗੀ\nname-invalid = ਨਾਂ \"{ $filename }\" ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ\nname-no-slashes = ਨਵਾਂ ਵਿੱਚ ਸਲੈਸ਼ ਨਹੀ ਹੋ ਸਕਦੀ ਹੈ\ncancel = ਰੱਦ ਕਰੋ\ncreate = ਬਣਾਓ\nopen = ਖੋਲ੍ਹੋ\nopen-file = ਫ਼ਾਇਲ ਨੂੰ ਖੋਲ੍ਹੋ\nopen-folder = ਫੋਲਡਰ ਨੂੰ ਖੋਲ੍ਹੋ\nopen-in-new-tab = ਨਵੀਂ ਟੈਬ ਵਿੱਚ ਖੋਲ੍ਹੋ\nopen-in-new-window = ਨਵੀਂ ਵਿੰਡੋ ਵਿੱਚ ਖੋਲ੍ਹੋ\ncosmic-files = COSMIC ਫ਼ਾਇਲਾਂ\noperations-running-finished =\n    { $running } { $running ->\n        [one] ਕਾਰਵਾਈ\n       *[other] ਕਾਰਵਾਈਆਂ\n    } ਚੱਲ ਰਹੀ ਹੈ ({ $percent }%), { $finished } ਮੁਕੰਮਲ...\nmount-error = ਡਰਾਇਵ ਵਰਤਣ ਲਈ ਅਸਮਰੱਥ\nopen-item-location = ਆਈਟਮ ਟਿਕਾਣੇ ਨੂੰ ਖੋਲ੍ਹੋ\nopen-multiple-files = ਕਈ ਫ਼ਾਇਲਾਂ ਨੂੰ ਖੋਲ੍ਹੋ\nopen-multiple-folders = ਕਈ ਫੋਲਡਰਾਂ ਨੂੰ ਖੋਲ੍ਹੋ\nsave = ਸੰਭਾਲੋ\nsave-file = ਫ਼ਾਇਲ ਨੂੰ ਸੰਭਾਲੋ\nopen-with-title = ਤੁਸੀ \"{ $name }\" ਨੂੰ ਕਿਵੇਂ ਖੋਲ੍ਹਣਾ ਚਾਹੁੰਦੇ ਹੋ?\nbrowse-store = { $store } ਦੀ ਝਲਕ ਵੇਖੋ\nother-apps = ਹੋਰ ਐਪਲੀਕੇਸ਼ਨਾਂ\nrelated-apps = ਸੰਬੰਧਿਤ ਐਪਲੀਕੇਸ਼ਨਾਂ\npermanently-delete-question = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਉਣਾ ਹੈ?\ndelete = ਹਟਾਓ\nrename-file = ਫ਼ਾਇਲ ਦਾ ਨਾਂ ਬਦਲੋ\nrename-folder = ਫੋਲਡਰ ਦਾ ਨਾਂ ਬਦਲੋ\nreplace = ਬਦਲੋ\noriginal-file = ਅਸਲ ਫ਼ਾਇਲ\nreplace-with = ਇਸ ਨਾਲ ਬਦਲੋ\napply-to-all = ਸਭ ਉੱਤੇ ਲਾਗੂ ਕਰੋ\nkeep-both = ਦੋਵਾਂ ਨੂੰ ਰੱਖੋ\nskip = ਛੱਡੋ\nset-and-launch = ਸੈੱਟ ਕਰੋ ਅਤੇ ਚਲਾਓ\nopen-with = ਇਸ ਨਾਲ ਖੋਲ੍ਹੋ\nowner = ਮਾਲਕ\ngroup = ਗਰੁੱਪ\nother = ਹੋਰ\nnone = ਕੋਈ ਨਹੀਂ\nexecute-only = ਸਿਰਫ਼ ਚੱਲਣ\nwrite-only = ਸਿਰਫ਼ ਲਿਖਣ\nwrite-execute = ਸਿਖਣ ਅਤੇ ਚੱਲਣ\nread-only = ਸਿਰਫ਼ ਪੜ੍ਹਨ\nread-execute = ਪੜ੍ਹਨ ਅਤੇ ਚੱਲਣ\nread-write = ਪੜ੍ਹਨ ਅਤੇ ਲਿਖਣ\nread-write-execute = ਪੜ੍ਹਨ, ਲਿਖਣ ਅਤੇ ਚੱਲਣ\nremove = ਹਟਾਓ\nkeep = ਰੱਖੋ\nrepository = ਰਿਪੋਜ਼ਟਰੀ\nsupport = ਸਹਿਯੋਗ\nconnect = ਕਨੈਕਟ ਕਰੋ\nremember-password = ਪਾਸਵਰਡ ਨੂੰ ਯਾਦ ਰੱਖੋ\ntry-again = ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ\nusername = ਵਰਤੋਂਕਾਰ ਦਾ ਨਾਂ\ncancelled = ਰੱਦ ਕੀਤਾ\nhistory = ਅਤੀਤ\npending = ਬਕਾਇਆ\nprogress = { $percent }%\nmenu-open-with = Open with...ਨਾਲ ਖੋਲ੍ਹੋ\ndefault-app = { $name } (ਮੂਲ)\nshow-details = ਵੇਰਵਿਆਂ ਨੂੰ ਵੇਖੋ\ntype = ਕਿਸਮ: { $mime }\nselected-items = { $items } ਚੁਣੀਆਂ ਆਈਟਮਾਂ\nreplace-title = \"{ $filename }\" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਮੌਜੂਦ ਹੈ\nfavorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ\nadd-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ\nconnect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ\nconnecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...\ndomain = ਡੋਮੇਨ\nenter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ\npassword = ਪਾਸਵਰਡ\nedit-history = ਸੋਧ ਦਾ ਅਤੀਤ\nprogress-cancelled = { $percent }%, ਰੱਦ ਕੀਤਾ\nprogress-failed = { $percent }%, ਅਸਫ਼ਲ ਹੈ\nprogress-paused = { $percent }%, ਵਿਰਾਮ ਹੈ\nfailed = ਅਸਫ਼ਲ\ncomplete = ਪੂਰਾ\nemptying-trash = { trash } ਨੂੰ ਖਾਲੀ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ ({ $progress })...\nemptied-trash = { trash } ਨੂੰ ਖਾਲੀ ਕੀਤਾ\nsettings = ਸੈਟਿੰਗਾਂ\nappearance = ਦਿੱਖ\ntheme = ਥੀਮ\nnew-file = ਨਵੀਂ ਫ਼ਾਇਲ...\nnew-folder = ਨਵਾਂ ਫੋਲਡਰ...\nopen-in-terminal = ਟਰਮੀਨਲ ਵਿੱਚ ਖੋਲ੍ਹੋ\nmove-to-trash = ਰੱਦੀ ਵਿੱਚ ਭੇਜੋ\nrestore-from-trash = ਰੱਦੀ ਵਿੱਚੋਂ ਬਹਾਲ ਕਰੋ\nsort-by-name = ਨਾਂ ਨਾਲ ਲੜੀਬੱਧ\nsort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ\nsort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ\nsort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ\nremove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ\nchange-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ...\ndesktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ...\ndisplay-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ...\nfile = ਫ਼ਾਇਲ\nnew-tab = ਨਵੀਂ ਟੈਬ\nnew-window = ਨਵੀਂ ਵਿੰਡੋ\nrename = ਨਾਂ ਨੂੰ ਬਦਲੋ...\nclose-tab = ਟੈਬ ਨੂੰ ਬੰਦ ਕਰੋ\nquit = ਬਾਹਰ\nedit = ਸੋਧੋ\ncut = ਕੱਟੋ\ncopy = ਕਾਪੀ ਕਰੋ\npaste = ਚੇਪੋ\nselect-all = ਸਭ ਨੂੰ ਚੁਣੋ\nzoom-in = ਜ਼ੂਮ ਇਨ\ndefault-size = ਮੂਲ ਆਕਾਰ\nzoom-out = ਜ਼ੂਮ ਆਉਟ\nview = ਵੇਖੋ\ngrid-view = ਗਰਿੱਡ ਝਲਕ\nshow-hidden-files = ਲੁਕਵੀਆਂ ਫ਼ਾਇਲਾਂ ਨੂੰ ਵੇਖੋ\nmenu-settings = ਸੈਟਿੰਗਾਂ...\nlist-directories-first = ਡਾਇਰੈਕਟਰੀਆਂ ਨੂੰ ਪਹਿਲਾਂ ਵੇਖੋ\nsort = ਲੜੀਬੱਧ\ngallery-preview = ਗੈਲਰੀ ਝਲਕ\nsort-newest-first = ਨਵੀਆਂ ਪਹਿਲਾਂ\nsort-oldest-first = ਪੁਰਾਣੀਆਂ ਪਹਿਲਾਂ\ncomment = COSMIC ਡੈਸਕਟਾਪ ਲਈ ਫ਼ਾਇਲ ਮੈਨੇਜਰ\nkeywords = ਫੋਲਡਰ;ਮੈਨੇਜਰ;\ncopy-to-title = ਕਾਪੀ ਕਰਨ ਲਈ ਟਿਕਾਣੇ ਨੂੁੰ ਚੁਣੋ\ncopy-to-button-label = ਕਾਪੀ ਕਰੋ\nmove-to-title = ਭੇਜਣ ਲਈ ਟਿਕਾਣੇ ਨੂੁੰ ਚੁਣੋ\nmove-to-button-label = ਭੇਜੋ\npermanently-delete-warning = { $target } ਨੂੰ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਇਆ ਜਾਵੇਗਾ। ਇਹ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਲਿਆ ਜਾ ਸਕਦਾ ਹੈ।\nreplace-warning-operation = ਕੀ ਤੁਸੀਂ ਇਸ ਨੂੰ ਬਦਲ ਦੇਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਨੂੰ ਬਦਲਣ ਨਾਲ ਸਮੱਗਰੀ ਉੱਤੇ ਲਿਖਿਆ ਜਾਵੇਗਾ।\nset-executable-and-launch = ਚੱਲਣਯੋਗ ਵਜੋਂ ਸੈੱਟ ਕਰੋ ਅਤੇ ਚਲਾਓ\nno-history = ਅਤੀਤ ਵਿੱਚ ਕੋਈ ਵੀ ਚੀਜ਼ ਨਹੀਂ ਹੈ।\npasted-text = ਲਿਖਤ ਨੂੰ ਚੇਪਿਆ\npasted-video = ਵੀਡੀਓ ਨੂੰ ਚੇਪਿਆ\ncreating = \"{ $parent }\" ਵਿੱਚ \"{ $name }\" ਨੂੰ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ\ncreated = \"{ $parent }\" ਵਿੱਚ \"{ $name }\" ਨੂੰ ਬਣਾਇਆ ਗਿਆ\nrenaming = \"{ $from }\" ਦਾ ਨਾਂ \"{ $to }\" ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ\nrenamed = \"{ $from }\" ਦਾ ਨਾਂ ਬਦਲ ਕੇ \"{ $to }\" ਕੀਤਾ ਗਿਆ\nitems = ਚੀਜ਼ਾਂ: { $items }\nitem-size = ਆਕਾਰ: { $size }\nitem-created = ਬਣਾਇਆ: { $created }\nitem-modified = ਸੋਧ ਕੀਤੀ: { $modified }\nitem-accessed = ਪਹੁੰਚ: { $accessed }\ncalculating = ਗਿਣਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ...\nsingle-click = ਇੱਕ ਵਾਰ ਕਲਿੱਕ ਕਰਕੇ ਖੋਲ੍ਹੋ\nshow-recents = ਬਾਹੀ ਵਿੱਚ ਸੱਜਰੇ ਫੋਲਡਰ\nmatch-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਦਾ\ndark = ਗੂੜ੍ਹਾ\nlight = ਹਲਕਾ\ntype-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ\ntype-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ\nadd-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ\ncompress = ਕੰਪਰੈਸ\ncopy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ...\ndelete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ\neject = ਬਾਹਰ\nextract-here = ਖਿਲਾਰੋ\nmove-to = ਇੱਥੇ ਭੇਜੋ...\nremove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ\nreload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ\nlist-view = ਸੂਚੀ ਝਲਕ\nmenu-about = COSMIC ਫਾਇਲਾਂ ਬਾਰੇ...\nsort-a-z = ਓ-ੜ\nsort-z-a = ੜ-ਓ\nsort-smallest-to-largest = ਛੋਟੇ ਤੋਂ ਵੱਡਾ\nsort-largest-to-smallest = ਵੱਡੇ ਤੋਂ ਛੋਟਾ\n"
  },
  {
    "path": "i18n/pl/cosmic_files.ftl",
    "content": "cosmic-files = Pliki COSMIC\ncomment = Menedżer plików pulpitu COSMIC\nkeywords = Katalogi;Pliki;Menedżer;\nempty-folder = Pusty katalog\nempty-folder-hidden = Pusty katalog (z ukrytymi plikami)\nno-results = Brak wyników\nfilesystem = System plików\nhome = Katalog domowy\nnetworks = Sieci\nnotification-in-progress = Operacje na plikach w toku\ntrash = Kosz\nrecents = Poprzednie\nundo = Cofnij\ntoday = Dzisiaj\n# Desktop view options\ndesktop-view-options = Opcje widoku pulpitu…\nshow-on-desktop = Pokaż na Pulpicie\ndesktop-folder-content = Zawartość katalogu Pulpit\nmounted-drives = Podpięte dyski\ntrash-folder-icon = Ikona kosza\nicon-size-and-spacing = Rozmiar i rozstaw ikon\nicon-size = Rozmiar ikon\ngrid-spacing = Rozstaw siatki\n# List view\nname = Nazwa\nmodified = Zmodyfikowano\ntrashed-on = Wyrzucono do kosza\nsize = Rozmiar\n# Progress footer\ndetails = Detale\ndismiss = Odrzuć wiadomość\noperations-running =\n    { $running } bieżące { $running ->\n        [one] działanie\n       *[other] działania\n    } ({ $percent }%)…\noperations-running-finished =\n    { $running } bieżące { $running ->\n        [one] działanie\n       *[other] działania\n    } ({ $percent }%), { $finished } ukończone…\npause = Wstrzymaj\nresume = Wznów\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Utwórz archiwum\n\n## Extract Dialog\n\nextract-password-required = Wymagane hasło\nextract-to = Wypakuj do…\nextract-to-title = Wypakuj do katalogu\n\n## Empty Trash Dialog\n\nempty-trash = Opróżnienie kosza\nempty-trash-warning = Elementy z kosza zostaną bezpowrotnie usunięte\n\n## Mount Error Dialog\n\nmount-error = Brak dostępu do dysku\n# New File/Folder Dialog\ncreate-new-file = Utwórz nowy plik\ncreate-new-folder = Utwórz nowy katalog\nfile-name = Nazwa pliku\nfolder-name = Nazwa katalogu\nfile-already-exists = Plik z taką nazwą już istnieje\nfolder-already-exists = Katalog z taką nazwą już istnieje\nname-hidden = Nazwy zaczynające się od „.” będą ukryte\nname-invalid = Musisz zmienić nazwę na inną z „{ $filename }”\nname-no-slashes = Nazwa nie może zawierać ukośników\n# Open/Save Dialog\ncancel = Anuluj\nopen = Otwórz\ncreate = Utwórz\nopen-file = Otwórz plik\nopen-folder = Otwórz katalog\nopen-in-new-tab = Otwórz w nowej karcie\nopen-in-new-window = Otwórz w nowym oknie\nopen-item-location = Otwórz położenie elementu\nopen-multiple-files = Otwórz wiele plików\nopen-multiple-folders = Otwórz wiele katalogów\nsave = Zapisz\nsave-file = Zapisz plik\n\n## Open With Dialog\n\nopen-with-title = Czym chcesz otworzyć „{ $name }”?\nbrowse-store = Przeglądaj { $store }\nother-apps = Inne aplikacje\nrelated-apps = Pokrewne aplikacje\n\n## Permanently delete Dialog\n\nselected-items = { $items } zaznaczonych elementów\npermanently-delete-question = Definitywnie usunąć?\ndelete = Usuń\npermanently-delete-warning = { $target } będzie bezpowrotnie usunięte. Nie będzie można tego przywrócić.\n# Rename Dialog\nrename-file = Zmień nazwę pliku\nrename-folder = Zmień nazwę katalogu\n# Replace Dialog\nreplace = Zastąp\nreplace-title = „{ $filename }” już istnieje w tym miejscu\nreplace-warning = Czy chcesz by został on zastąpiony przez wybrany element? To nadpisze jego zawartość.\nreplace-warning-operation = Czy chcesz by został on zastąpiony? To nadpisze jego zawartość.\noriginal-file = Oryginalny plik\nreplace-with = Zastąpiony przez\napply-to-all = Zastosuj do wszystkich\nkeep-both = Zachowaj oba\nskip = Pomiń\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Ustaw jako wykonywalny i uruchom\nset-executable-and-launch-description = Czy chcesz ustawić plik „{ $name }” jako wykonywalny i uruchomić go?\nset-and-launch = Ustaw i uruchom\n\n## Metadata Dialog\n\nopen-with = Otwórz za pomocą\nowner = Właściciel\ngroup = Grupa\nother = Inni\n\n### Mode 0\n\nnone = Brak\n\n### Mode 1 (unusual)\n\nexecute-only = Tylko wykonywanie\n\n### Mode 2 (unusual)\n\nwrite-only = Tylko zapis\n\n### Mode 3 (unusual)\n\nwrite-execute = Zapis i wykonywanie\n\n### Mode 4\n\nread-only = Tylko odczyt\n\n### Mode 5\n\nread-execute = Odczyt i wykonywanie\n\n### Mode 6\n\nread-write = Odczyt i zapis\n\n### Mode 7\n\nread-write-execute = Odczyt, zapis i wykonywanie\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Błąd podczas otwierania katalogu\nfavorite-path-error-description =\n    Nie można otworzyć „{ $path }”\n    Może nie istnieć lub możesz nie mieć uprawnień do jego otwierania\n\n    Czy chcesz go usunąć z panelu bocznego?\nremove = Usuń\nkeep = Zachowaj\n\n# Context Pages\n\n\n## About\n\nrepository = Repozytorium\nsupport = Wsparcie\n\n## Add Network Drive\n\nadd-network-drive = Dodaj dysk sieciowy\nconnect = Połącz\nconnect-anonymously = Połącz anonimowo\nconnecting = Łączenie…\ndomain = Domena\nenter-server-address = Wprowadź adres serwera\nnetwork-drive-description =\n    Adres serwera zawiera prefiks protokołu i adres.\n    Przykładowo: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Dostępne protokoły,Prefiks\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDAV,dav:// or davs://\nnetwork-drive-error = Brak dostępu do dysku sieciowego\npassword = Hasło\nremember-password = Zapamiętaj hasło\ntry-again = Spróbuj ponownie\nusername = Nazwa użytkownika\n\n## Operations\n\ncancelled = Anulowano\nedit-history = Historia edycji\nhistory = Historia\nno-history = Brak pozycji w historii.\npending = Oczekujące\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, anulowano\nprogress-failed = { $percent }%, nieudane\nprogress-paused = { $percent }%, wstrzymano\nfailed = Nieudane\ncomplete = Ukończone\ncompressing =\n    Spakuj { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z „{ $from }” do „{ $to }” ({ $progress })…\ncompressed =\n    Spakowano { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z „{ $from }” do „{ $to }”\ncopy_noun = Kopiuj\ncreating = Tworzy „{ $name }” w „{ $parent }”\ncreated = Stworzono „{ $name }” w „{ $parent }”\ncopying =\n    Kopiowanie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z „{ $from }” do „{ $to }” ({ $progress })…\ncopied =\n    Skopiowano { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z „{ $from }” do „{ $to }”\ndeleting =\n    Usuwanie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z { trash } ({ $progress })...\ndeleted =\n    Usunięto { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z { trash }\nemptying-trash = Opróżnianie { trash } ({ $progress })…\nemptied-trash = Opróżniono { trash }\nextracting =\n    Wypakowywanie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z „{ $from }” do „{ $to }” ({ $progress })…\nextracted =\n    Wypakowano { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z „{ $from }” do „{ $to }”\nsetting-executable-and-launching = Ustawianie „{ $name }” jako wykonywalnego i uruchamianie\nset-executable-and-launched = Ustaw „{ $name }” jako wykonywalny i uruchom\nsetting-permissions = Ustawianie uprawnień dla „{ $name }” na { $mode }\nset-permissions = Ustaw uprawnienia dla „{ $name }” na { $mode }\nmoving =\n    Przenoszenie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z „{ $from }” do „{ $to }” ({ $progress })…\nmoved =\n    Przeniesiono { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z „{ $from }” do „{ $to }”\npermanently-deleting =\n    Definitywne usuwanie \"{ $items }\" \"{ $items ->\n        [one] elementu\n       *[other] elementów\n    }\"\npermanently-deleted =\n    Definitywnie usunięto \"{ $items }\" \"{ $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    }\"\nremoving-from-recents =\n    Usuwanie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z Poprzednich\nremoved-from-recents =\n    Usunięto { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z Poprzednich\nrenaming = Zmieniana nazwa z „{ $from }” na „{ $to }”\nrenamed = Zmieniono nazwę z „{ $from }” na „{ $to }”\nrestoring =\n    Przywracanie { $items } { $items ->\n        [one] elementu\n       *[other] elementów\n    } z Kosza ({ $progress })…\nrestored =\n    Przywrócono { $items } { $items ->\n        [one] element\n        [few] elementy\n       *[other] elementów\n    } z Kosza\nunknown-folder = nieznany katalog\n\n## Open with\n\nmenu-open-with = Otwórz za pomocą…\ndefault-app = { $name } (domyślnie)\n\n## Show details\n\nshow-details = Pokaż szczegóły\ntype = Typ: { $mime }\nitems = Elementy: { $items }\nitem-size = Rozmiar: { $size }\nitem-created = Utworzono: { $created }\nitem-modified = Zmodyfikowano: { $modified }\nitem-accessed = Otwarto: { $accessed }\ncalculating = Obliczanie…\n\n## Settings\n\nsettings = Ustawienia\nsingle-click = Jedno kliknięcie by otwierać\n\n### Appearance\n\nappearance = Wygląd\ntheme = Motyw\nmatch-desktop = Dopasuj do Pulpitu\ndark = Ciemny\nlight = Jasny\n\n### Type to Search\n\ntype-to-search = Zacznij pisać by wyszukać\ntype-to-search-recursive = Wyszukuj w obecnym katalogu i jego podkatalogach\ntype-to-search-enter-path = Wprowadź ścieżkę pliku lub katalogu\n# Context menu\nadd-to-sidebar = Dodaj do bocznego panelu\ncompress = Spakuj…\ndelete-permanently = Usuń definitywnie\neject = Wysuń\nextract-here = Wypakuj\nnew-file = Nowy plik...\nnew-folder = Nowy katalog...\nopen-in-terminal = Otwórz w terminalu\nmove-to-trash = Przenieś do kosza\nrestore-from-trash = Przywróć z kosza\nremove-from-sidebar = Usuń z bocznego panelu\nsort-by-name = Uszereguj według nazwy\nsort-by-modified = Uszereguj według czasu modyfikacji\nsort-by-size = Uszereguj według rozmiaru\nsort-by-trashed = Uszereguj według czasu usunięcia\nremove-from-recents = Usuń z poprzednich\n\n## Desktop\n\nchange-wallpaper = Zmień tapetę…\ndesktop-appearance = Wygląd pulpitu…\ndisplay-settings = Ustawienia wyświetlacza…\n\n# Menu\n\n\n## File\n\nfile = Plik\nnew-tab = Nowa karta\nnew-window = Nowe okno\nreload-folder = Odśwież katalog\nrename = Zmień nazwę…\nclose-tab = Zamknij kartę\nquit = Zamknij\n\n## Edit\n\nedit = Edytuj\ncut = Wytnij\ncopy = Kopiuj\npaste = Wklej\nselect-all = Zaznacz wszystko\n\n## View\n\nzoom-in = Zbliż\ndefault-size = Domyślny rozmiar\nzoom-out = Oddal\nview = Widok\ngrid-view = Widok siatki\nlist-view = Widok listy\nshow-hidden-files = Pokaż ukryte pliki\nlist-directories-first = Najpierw wyświetlaj katalogi\ngallery-preview = Podgąd galerii\nmenu-settings = Ustawienia…\nmenu-about = O Plikach COSMIC…\n\n## Sort\n\nsort = Uszereguj\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Najpierw najnowsze\nsort-oldest-first = Najpierw najstarsze\nsort-smallest-to-largest = Najpierw najmniejsze\nsort-largest-to-smallest = Najpierw największe\nempty-trash-title = Opróżnić kosz?\ntype-to-search-select = Wybierz pierwszy pasujący plik lub katalog\npasted-image = Wklej Obraz\npasted-text = Wklejony Tekst\npasted-video = Wklejone Wideo\ncopy-to-title = Wybierz gdzie skopiować\ncopy-to-button-label = Kopiuj\nmove-to-title = Wybierz gdzie przenieść\nmove-to-button-label = Przenieś\ncopy-to = Skopiuj do…\nmove-to = Przenieś do…\nshow-recents = Ostatnie katalogi w panelu bocznym\nclear-recents-history = Wyczyść bierzącą historię\ncopy-path = Skopiuj ścieżkę\nmixed = Mieszane\n"
  },
  {
    "path": "i18n/pt/cosmic_files.ftl",
    "content": "cosmic-files = Ficheiros COSMIC\ncomment = Gerenciador de arquivos do COSMIC\nkeywords = Pastas;Gerenciador;Arquivos;Gestor;Explorer;\nempty-folder = Pasta vazia\nempty-folder-hidden = Pasta vazia (tem ficheiros ocultos)\nno-results = Nenhum resultado encontrado\nfilesystem = Sistema de ficheiros\nhome = Pasta Pessoal\nnotification-in-progress = Operações em curso.\ntrash = Lixo\nundo = Anular\n# List view\nname = Nome\nmodified = Modificado\nsize = Tamanho\n\n# Dialogs\n\n\n## Empty Trash Dialog\n\nempty-trash = Esvaziar lixo\nempty-trash-warning = Pretende eliminar permanentemente todos os itens do Lixo?\n# New File/Folder Dialog\ncreate-new-file = Criar novo ficheiro\ncreate-new-folder = Criar nova pasta\nfile-name = Nome do ficheiro\nfolder-name = Nome da pasta\nfile-already-exists = Já existe um ficheiro com esse nome.\nfolder-already-exists = Já existe uma pasta com esse nome.\nname-hidden = Os nomes começados por \".\" serão ocultados.\nname-invalid = O nome não pode ser \"{ $filename }\".\nname-no-slashes = O nome não pode conter barras.\n# Open/Save Dialog\ncancel = Cancelar\nopen = Abrir\nopen-file = Abrir ficheiro\nopen-folder = Abrir pasta\nopen-in-new-tab = Abrir num novo separador\nopen-in-new-window = Abrir numa nova janela\nopen-item-location = Abrir localização do item\nopen-multiple-files = Abrir vários ficheiros\nopen-multiple-folders = Abrir várias pastas\nsave = Guardar\nsave-file = Guardar ficheiro\n# Rename Dialog\nrename-file = Renomear ficheiro\nrename-folder = Renomear pasta\n# Replace Dialog\nreplace = Substituir\nreplace-title = { $filename } já existe neste local.\nreplace-warning = Substituí-lo pelo que está a guardar? Se o substituir, o seu conteúdo será substituído.\nreplace-warning-operation = Pretende substituí-lo? Ao substituí-lo, o seu conteúdo será substituído.\noriginal-file = Ficheiro original\nreplace-with = Substituir por\napply-to-all = Aplicar a tudo\nkeep-both = Manter ambos\nskip = Ignorar\n\n## Metadata Dialog\n\nowner = Proprietário\ngroup = Grupo\nother = Outro\n\n# Context Pages\n\n\n## About\n\n\n## Operations\n\nedit-history = Editar histórico\nhistory = Histórico\nno-history = Nenhum item no histórico.\npending = Pendentes\nfailed = Com falha\ncomplete = Concluído\ncopy_noun = Copiado\ncreating = A criar \"{ $name }\" em \"{ $parent }\"\ncreated = \"{ $name }\" criado em \"{ $parent }\"\ncopying =\n    A copiar { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] item copiado\n       *[other] itens copiados\n    } de \"{ $from }\" para \"{ $to }\"\nemptying-trash = A esvaziar { trash } ({ $progress })...\nemptied-trash = { trash } esvaziado\nextracting =\n    A extrair { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" par \"{ $to }\" ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] item extraído\n       *[other] itens extraídos\n    } de \"{ $from }\" para \"{ $to }\"\nmoving =\n    A mover { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] item movido\n       *[other] itens movidos\n    } de \"{ $from }\" para \"{ $to }\"\nrenaming = A renomear \"{ $from }\" para \"{ $to }\"\nrenamed = \"{ $from }\" renomeado para \"{ $to }\"\nrestoring =\n    A restaurar { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de { trash } ({ $progress })...\nrestored =\n    Restaurado { $items } { $items ->\n        [one] item\n       *[other] itens\n    } para { trash }\nunknown-folder = pasta desconhecida\n\n## Open with\n\nmenu-open-with = Abrir com...\ndefault-app = { $name } (predefinição)\n\n## Show details\n\nshow-details = Mostrar detalhes\n\n## Settings\n\nsettings = Definições\n\n### Appearance\n\nappearance = Aparência\ntheme = Tema\nmatch-desktop = Estilo do sistema\ndark = Escuro\nlight = Claro\n# Context menu\nextract-here = Extrair\nadd-to-sidebar = Adicionar à barra lateral\nnew-file = Novo ficheiro...\nnew-folder = Nova pasta...\nopen-in-terminal = Abrir no terminal\nmove-to-trash = Mover para o lixo\nrestore-from-trash = Restaurar do lixo\nremove-from-sidebar = Remover da barra lateral\nsort-by-name = Ordenar por nome\nsort-by-modified = Ordenar por data de modificação\nsort-by-size = Ordenar por tamanho\n\n# Menu\n\n\n## File\n\nfile = Ficheiro\nnew-tab = Novo separador\nnew-window = Nova janela\nrename = Renomear...\nclose-tab = Fechar separador\nquit = Sair\n\n## Edit\n\nedit = Editar\ncut = Cortar\ncopy = Copiar\npaste = Colar\nselect-all = Selecionar tudo\n\n## View\n\nzoom-in = Aumentar\ndefault-size = Tamanho predefinido\nzoom-out = Diminuir\nview = Ver\ngrid-view = Visualização em grelha\nlist-view = Visualização em lista\nshow-hidden-files = Mostrar ficheiros ocultos\nlist-directories-first = Listar primeiro os diretórios\nmenu-settings = Definições...\nmenu-about = Acerca do Ficheiros COSMIC...\nrepository = Repositório\nsupport = Suporte\ndetails = Detalhes\ndismiss = Dispensar mensagem\nremove = Remover\ncancelled = Canceladas\nnetworks = Redes\nrecents = Recentes\ntoday = Hoje\ndesktop-view-options = Opções de visualização da área de trabalho...\nshow-on-desktop = Mostrar na área de trabalho\ndesktop-folder-content = Conteúdo da pasta da área de trabalho\nmounted-drives = Dispositivos montados\ntrash-folder-icon = Ícone do lixo\nicon-size-and-spacing = Tamanho e espaçamento do ícone\nicon-size = Tamanho do ícone\ngrid-spacing = Espaçamento entre ícones\ntrashed-on = Enviado para o lixo\noperations-running =\n    { $running } { $running ->\n        [one] operação\n       *[other] operações\n    } em execução ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operação\n       *[other] operações\n    } em execução ({ $percent }%), { $finished } concluídas...\npause = Pausa\nresume = Retomar\ncreate-archive = Criar arquivo\nextract-password-required = Palavra-passe necessária\nextract-to = Extrair para...\nextract-to-title = Extrair para pasta\nmount-error = Não foi possível aceder ao dispositivo\ncreate = Criar\nopen-with-title = Como pretende abrir \"{ $name }\"?\nbrowse-store = Procurar em { $store }\nother-apps = Outras aplicações\nrelated-apps = Aplicações relacionadas\nselected-items = os { $items } itens selecionados\npermanently-delete-question = Eliminar permanentemente\ndelete = Eliminar\npermanently-delete-warning = Tem a certeza de que pretende eliminar { $target } permanentemente? Esta ação não pode ser anulada.\nset-executable-and-launch = Definir como executável e iniciar\nset-executable-and-launch-description = Pretende definir \"{ $name }\" como executável e iniciá-lo?\nset-and-launch = Definir e iniciar\nopen-with = Abrir com\nnone = Nenhum(a)\nexecute-only = Executar-apenas\nwrite-only = Gravar-apenas\nwrite-execute = Gravação e execução\nread-only = Apenas-leitura\nread-execute = Leitura e execução\nread-write = Leitura e escrita\nread-write-execute = Leitura, gravação e execução\nfavorite-path-error = Erro ao abrir diretório\nfavorite-path-error-description =\n    Não foi possível abrir \"{ $path }\".\n    O item pode não existir ou não tem permissão para abri-lo.\n\n    Pretende removê-lo da barra lateral?\nkeep = Manter\nadd-network-drive = Adicionar unidade de rede\nconnect = Ligar\nconnect-anonymously = Ligar anonimamente\nconnecting = A ligar…\ndomain = Domínio\nenter-server-address = Insira o endereço do servidor\nnetwork-drive-description =\n    Endereços de servidor incluem um prefixo de protocolo e um endereço.\n    Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Protocolos disponíveis,Prefixo\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// ou ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// ou ssh://\n    WebDAV,dav:// ou davs://\nnetwork-drive-error = Não é possível aceder à unidade de rede\npassword = Palavra-passe\nremember-password = Memorizar palavra-passe\ntry-again = Tentar novamente\nusername = Nome de utilizador\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, cancelado\nprogress-failed = { $percent }%, com falha\nprogress-paused = { $percent }%, em pausa\ncompressing =\n    A comprimir { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] item comprimido\n       *[other] itens comprimidos\n    } de \"{ $from }\" para \"{ $to }\"\ndeleting =\n    A eliminar { $items } { $items ->\n        [one] item\n       *[other] itens\n    } do { trash } ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] item eliminado\n       *[other] itens eliminados\n    } do { trash }\nsetting-executable-and-launching = A definir \"{ $name }\" como executável e a iniciar\nset-executable-and-launched = \"{ $name }\" definido como executável e iniciado\nsetting-permissions = A definir permissões de \"{ $name }\" para { $mode }\nset-permissions = Definir permissões de \"{ $name }\" para { $mode }\npermanently-deleting =\n    A eliminar permanentemente { $items } { $items ->\n        [one] item\n       *[other] itens\n    }\npermanently-deleted =\n    { $items } { $items ->\n        [one] item eliminado\n       *[other] itens eliminados\n    } permanentemente\nremoving-from-recents =\n    A remover { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de { recents }\nremoved-from-recents =\n    { $items } { $items ->\n        [one] item removido\n       *[other] itens removidos\n    } de { recents }\ntype = Tipo: { $mime }\nitems = Itens: { $items }\nitem-size = Tamanho: { $size }\nitem-created = Criado: { $created }\nitem-modified = Modificado: { $modified }\nitem-accessed = Acedido: { $accessed }\ncalculating = A calcular...\nsingle-click = Um único clique para abrir\ntype-to-search = Escreva para pesquisar\ntype-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas\ntype-to-search-enter-path = Insere o caminho do diretório ou ficheiro\ncompress = Comprimir\ndelete-permanently = Eliminar permanentemente\neject = Ejetar\nsort-by-trashed = Ordenar por data de eliminação\nremove-from-recents = Remover dos itens recentes\nchange-wallpaper = Alterar papel de parede...\ndesktop-appearance = Aparência da área de trabalho...\ndisplay-settings = Definições do ecrã...\nreload-folder = Recarregar pasta\ngallery-preview = Pré-visualizar\nsort = Ordenar\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Mais recentes primeiro\nsort-oldest-first = Mais antigos primeiro\nsort-smallest-to-largest = Do menor para o maior\nsort-largest-to-smallest = Do maior para o menor\n"
  },
  {
    "path": "i18n/pt-BR/cosmic_files.ftl",
    "content": "cosmic-files = Gestor de Arquivos\ncomment = Gerenciador de arquivos do ambiente COSMIC\nempty-folder = Pasta vazia\nempty-folder-hidden = Pasta vazia (contém itens ocultos)\nno-results = Nenhum item encontrado\nfilesystem = Sistema de arquivos\nhome = Pasta pessoal\nnetworks = Redes\nnotification-in-progress = Há operações de arquivo em andamento\ntrash = Lixeira\nrecents = Recentes\nundo = Desfazer\ntoday = Hoje\n# Desktop view options\ndesktop-view-options = Opções de visualização da área de trabalho...\nshow-on-desktop = Mostrar na Área de trabalho\ndesktop-folder-content = Conteúdo da pasta da área de trabalho\nmounted-drives = Dispositivos montados\ntrash-folder-icon = Ícone da lixeira\nicon-size-and-spacing = Tamanho e espaçamento do ícone\nicon-size = Tamanho do ícone\ngrid-spacing = Espaçamento entre ícones\n# List view\nname = Nome\nmodified = Modificado\ntrashed-on = Enviado à lixeira\nsize = Tamanho\n# Progress footer\ndetails = Detalhes\ndismiss = Dispensar mensagem\noperations-running =\n    { $running } { $running ->\n        [one] operação\n       *[other] operações\n    } em andamento ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operação\n       *[other] operações\n    } em andamento ({ $percent }%), { $finished } concluídas...\npause = Pausar\nresume = Continuar\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Compactar arquivos\n\n## Extract Dialog\n\nextract-password-required = Senha necessária\nextract-to = Extrair para...\nextract-to-title = Extrair para pasta\n\n## Empty Trash Dialog\n\nempty-trash = Esvaziar a lixeira\nempty-trash-warning = Todos os itens da Lixeira serão permanentemente excluídos\n\n## Mount Error Dialog\n\nmount-error = Não é possível acessar a unidade\n\n## New File/Folder Dialog\n\ncreate-new-file = Criar novo arquivo\ncreate-new-folder = Criar nova pasta\nfile-name = Nome do arquivo\nfolder-name = Nome da pasta\nfile-already-exists = Já existe um arquivo com esse nome\nfolder-already-exists = Já existe uma pasta com esse nome\nname-hidden = Os nomes que começam com \".\" serão ocultados\nname-invalid = O nome não pode ser \"{ $filename }\"\nname-no-slashes = O nome não pode conter barras\n\n## Open/Save Dialog\n\ncancel = Cancelar\ncreate = Criar\nopen = Abrir\nopen-file = Abrir arquivo\nopen-folder = Abrir pasta\nopen-in-new-tab = Abrir em uma nova aba\nopen-in-new-window = Abrir em uma nova janela\nopen-item-location = Abrir local do item\nopen-multiple-files = Abrir vários arquivos\nopen-multiple-folders = Abrir várias pastas\nsave = Salvar\nsave-file = Salvar arquivo\n\n## Open With Dialog\n\nopen-with-title = Como deseja abrir \"{ $name }\"?\nbrowse-store = Procurar em { $store }\nother-apps = Outros aplicativos\nrelated-apps = Aplicativos relacionados\n\n## Permanently delete Dialog\n\nselected-items = Os { $items } itens selecionados\npermanently-delete-question = Excluir permanentemente?\ndelete = Excluir\npermanently-delete-warning = Deseja realmente excluir permanentemente { $target }? Esta operação não poderá ser desfeita.\n\n## Rename Dialog\n\nrename-file = Renomear arquivo\nrename-folder = Renomear pasta\n\n## Replace Dialog\n\nreplace = Substituir\nreplace-title = \"{ $filename }\" já existe neste local\nreplace-warning = Deseja substituí-lo por aquele que está salvando? Ao substituí-lo, seu conteúdo será sobrescrito.\nreplace-warning-operation = Deseja substituí-lo? Ao substituí-lo, seu conteúdo será sobrescrito.\noriginal-file = Arquivo original\nreplace-with = Substituir por\napply-to-all = Aplicar a todos\nkeep-both = Manter ambos\nskip = Ignorar\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Definir como executável e iniciar\nset-executable-and-launch-description = Deseja definir \"{ $name }\" como executável e iniciá-lo?\nset-and-launch = Marcar e iniciar\n\n## Metadata Dialog\n\nopen-with = Abrir com\nowner = Proprietário\ngroup = Grupo\nother = Outros\n\n### Mode 0\n\nnone = Nenhum\n\n### Mode 1 (unusual)\n\nexecute-only = Somente execução\n\n### Mode 2 (unusual)\n\nwrite-only = Somente escrita\n\n### Mode 3 (unusual)\n\nwrite-execute = Escrita e execução\n\n### Mode 4\n\nread-only = Somente leitura\n\n### Mode 5\n\nread-execute = Leitura e execução\n\n### Mode 6\n\nread-write = Leitura e escrita\n\n### Mode 7\n\nread-write-execute = Leitura, escrita e execução\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Erro ao abrir diretório\nfavorite-path-error-description =\n    Não foi possível abrir \"{ $path }\"\n    \"{ $path }\" pode não existir ou você não tem permissão para abri-lo\n\n    Deseja removê-lo da barra lateral?\nremove = Remover\nkeep = Manter\n\n# Context Pages\n\n\n## About\n\nrepository = Repositório\nsupport = Suporte\n\n## Add Network Drive\n\nadd-network-drive = Adicionar local de rede\nconnect = Conectar\nconnect-anonymously = Conectar anonimamente\nconnecting = Conectando...\ndomain = Domínio\nenter-server-address = Insira o endereço do servidor\nnetwork-drive-description =\n    Endereços de servidor incluem um prefixo de protocolo e um endereço.\n    Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Certifique-se de manter a vírgula que separa as colunas\n\nnetwork-drive-schemes =\n    Protocolos disponíveis,Prefixo\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// ou ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// ou ssh://\n    WebDAV,dav:// ou davs://\nnetwork-drive-error = Não foi possível acessar o local de rede\npassword = Senha\nremember-password = Lembrar senha\ntry-again = Tente novamente\nusername = Usuário\n\n## Operations\n\ncancelled = Cancelado\nedit-history = Editar histórico\nhistory = Histórico\nno-history = Nenhum item no histórico.\npending = Pendente\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, cancelado\nprogress-failed = { $percent }%, com falha\nprogress-paused = { $percent }%, pausado\nfailed = Com falha\ncomplete = Concluído\ncompressing =\n    Compactando { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] item compactado\n       *[other] itens compactados\n    } de \"{ $from }\" para \"{ $to }\"\ncopy_noun = Copiar\ncreating = Criando \"{ $name }\" em \"{ $parent }\"\ncreated = \"{ $name }\" criado em \"{ $parent }\"\ncopying =\n    Copiando { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] item copiado\n       *[other] itens copiados\n    } de \"{ $from }\" para \"{ $to }\"\ndeleting =\n    Excluindo { $items } { $items ->\n        [one] item\n       *[other] itens\n    } da { trash } ({ $progress })...\ndeleted =\n    { $items } { $items ->\n        [one] item excluído\n       *[other] itens excluídos\n    } da { trash }\nemptying-trash = Esvaziando a { trash } ({ $progress })...\nemptied-trash = { trash } esvaziada\nextracting =\n    Extraindo { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] item extraído\n       *[other] itens extraídos\n    } de \"{ $from }\" para \"{ $to }\"\nsetting-executable-and-launching = Marcando \"{ $name }\" como executável e iniciando\nset-executable-and-launched = \"{ $name }\" marcado como executável e iniciado\nsetting-permissions = Definindo permissões de \"{ $name }\" para { $mode }\nset-permissions = Definir permissões de \"{ $name }\" para { $mode }\nmoving =\n    Movendo { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de \"{ $from }\" para \"{ $to }\" ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] item movido\n       *[other] itens movidos\n    } de \"{ $from }\" para \"{ $to }\"\npermanently-deleting =\n    Excluindo permanentemente { $items } { $items ->\n        [one] item\n       *[other] itens\n    }\npermanently-deleted =\n    { $items } { $items ->\n        [one] item excluído\n       *[other] itens excluídos\n    } permanentemente\nremoving-from-recents =\n    Removendo { $items } { $items ->\n        [one] item\n       *[other] itens\n    } de { recents }\nremoved-from-recents =\n    { $items } { $items ->\n        [one] item removido\n       *[other] itens removidos\n    } de { recents }\nrenaming = Renomeando \"{ $from }\" para \"{ $to }\"\nrenamed = \"{ $from }\" renomeado para \"{ $to }\"\nrestoring =\n    Restaurando { $items } { $items ->\n        [one] item\n       *[other] itens\n    } da { trash } ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] item\n       *[other] itens\n    } da { trash } restaurado(s)\nunknown-folder = pasta desconhecida\n\n## Open with\n\nmenu-open-with = Abrir com...\ndefault-app = { $name } (padrão)\n\n## Show details\n\nshow-details = Mostrar detalhes\ntype = Tipo: { $mime }\nitems = Itens: { $items }\nitem-size = Tamanho: { $size }\nitem-created = Criado: { $created }\nitem-modified = Modificado: { $modified }\nitem-accessed = Acessado: { $accessed }\ncalculating = Calculando...\n\n## Settings\n\nsettings = Configurações\nsingle-click = Clique simples para abrir\n\n### Appearance\n\nappearance = Aparência\ntheme = Tema\nmatch-desktop = Estilo do sistema\ndark = Estilo escuro\nlight = Estilo claro\n\n### Type to Search\n\ntype-to-search = Digite para pesquisar\ntype-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas\ntype-to-search-enter-path = Insere o caminho do diretório ou arquivo\n# Context menu\nadd-to-sidebar = Adicionar à barra lateral\ncompress = Compactar...\ndelete-permanently = Excluir permanentemente\neject = Desmontar\nextract-here = Extrair\nnew-file = Novo arquivo...\nnew-folder = Nova pasta...\nopen-in-terminal = Abrir no terminal\nmove-to-trash = Mover para a lixeira\nrestore-from-trash = Restaurar da lixeira\nremove-from-sidebar = Remover da barra lateral\nsort-by-name = Ordenar por nome\nsort-by-modified = Ordenar por data de modificação\nsort-by-size = Ordenar por tamanho\nsort-by-trashed = Ordernar por data de exclusão\nremove-from-recents = Remover dos itens recentes\n\n## Desktop\n\nchange-wallpaper = Alterar o plano de fundo...\ndesktop-appearance = Aparência da área de trabalho...\ndisplay-settings = Configurações da tela...\n\n# Menu\n\n\n## File\n\nfile = Arquivo\nnew-tab = Nova aba\nnew-window = Nova janela\nreload-folder = Recarregar pasta\nrename = Renomear...\nclose-tab = Fechar aba\nquit = Sair\n\n## Edit\n\nedit = Editar\ncut = Recortar\ncopy = Copiar\npaste = Colar\nselect-all = Selecionar tudo\n\n## View\n\nzoom-in = Ampliar\ndefault-size = Tamanho padrão\nzoom-out = Reduzir\nview = Exibir\ngrid-view = Exibição em grade\nlist-view = Exibição em lista\nshow-hidden-files = Mostrar arquivos ocultos\nlist-directories-first = Listar pastas primeiro\ngallery-preview = Pré-visualizar\nmenu-settings = Configurações...\nmenu-about = Sobre o Gestor de Arquivos...\n\n## Sort\n\nsort = Ordenar\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Mais novos primeiro\nsort-oldest-first = Mais antigos primeiro\nsort-smallest-to-largest = Do menor para o maior\nsort-largest-to-smallest = Do maior para o menor\nempty-trash-title = Esvaziar a lixeira?\ntype-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente\npasted-image = Imagem colada\npasted-text = Texto colado\npasted-video = Vídeo colado\ncopy-to-title = Selecione o destino da cópia\ncopy-to-button-label = Copiar\nmove-to-title = Selecione o destino da movimentação\nmove-to-button-label = Mover\ncopy-to = Copiar para...\nmove-to = Mover para...\nkeywords = Pasta;Gerenciador;\nshow-recents = Pasta de recentes na barra lateral\nclear-recents-history = Limpar histórico de recentes\ncopy-path = Copiar caminho\nmixed = Misto\n"
  },
  {
    "path": "i18n/ro/cosmic_files.ftl",
    "content": "cosmic-files = Fișiere COSMIC\nempty-folder = Dosar gol\nempty-folder-hidden = Dosar gol (conține elemente ascunse)\nno-results = Niciun rezultat găsit\nfilesystem = Sistem de fișiere\nhome = Acasă\nnetworks = Rețele\nnotification-in-progress = Operațiuni de fișiere în desfășurare.\ntrash = Coș de gunoi\nrecents = Recente\nundo = Anulează\ntoday = Astăzi\n# Desktop view options\ndesktop-view-options = Opțiuni de vizualizare desktop...\nshow-on-desktop = Afișează pe desktop\ndesktop-folder-content = Conținut dosar desktop\nmounted-drives = Unități montate\ntrash-folder-icon = Pictogramă coș de gunoi\nicon-size-and-spacing = Dimensiune și spațiere pictograme\nicon-size = Dimensiune pictogramă\ngrid-spacing = Spațiere grilă\n# List view\nname = Nume\nmodified = Modificat\ntrashed-on = Șters\nsize = Dimensiune\n# Progress footer\ndetails = Detalii\ndismiss = Închide mesajul\noperations-running = { $running } operațiuni în desfășurare ({ $percent }%)...\noperations-running-finished = { $running } operațiuni în desfășurare ({ $percent }%), { $finished } finalizate...\npause = Pauză\nresume = Reia\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Creează o arhivă\n\n## Extract Dialog\n\nextract-password-required = Parolă necesară\nextract-to = Extrage în...\nextract-to-title = Extrage în dosar\n\n## Empty Trash Dialog\n\nempty-trash = Golește coșul\nempty-trash-warning = Sigur dorești să ștergi definitiv toate elementele din coș?\n\n## Dialog Eroare Montare\n\nmount-error = Nu se poate accesa unitatea\n\n## New File/Folder Dialog\n\ncreate-new-file = Creează un fișier nou\ncreate-new-folder = Creează un dosar nou\nfile-name = Nume fișier\nfolder-name = Nume dosar\nfile-already-exists = Un fișier cu acest nume există deja.\nfolder-already-exists = Un dosar cu acest nume există deja.\nname-hidden = Numele care încep cu „.” vor fi ascunse.\nname-invalid = Numele nu poate fi \"{ $filename }\".\nname-no-slashes = Numele nu poate conține caractere „/”.\n\n## Open/Save Dialog\n\ncancel = Anulează\ncreate = Creează\nopen = Deschide\nopen-file = Deschide fișier\nopen-folder = Deschide dosar\nopen-in-new-tab = Deschide în filă nouă\nopen-in-new-window = Deschide în fereastră nouă\nopen-item-location = Deschide locația elementului\nopen-multiple-files = Deschide fișiere multiple\nopen-multiple-folders = Deschide dosare multiple\nsave = Salvează\nsave-file = Salvează fișier\n\n## Open With Dialog\n\nopen-with-title = Cum dorești să deschizi „{ $name }”?\nbrowse-store = Răsfoiește în { $store }\n\n## Rename Dialog\n\nrename-file = Redenumește fișier\nrename-folder = Redenumește dosar\n\n## Replace Dialog\n\nreplace = Înlocuiește\nreplace-title = „{ $filename }” există deja în această locație.\nreplace-warning = Dorești să-l înlocuiești cu cel pe care îl salvezi? Această acțiune va suprascrie conținutul.\nreplace-warning-operation = Dorești să-l înlocuiești? Această acțiune va suprascrie conținutul.\noriginal-file = Fișier original\nreplace-with = Înlocuiește cu\napply-to-all = Aplică la toate\nkeep-both = Păstrează ambele\nskip = Omitere\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Fă executabil și rulează\nset-executable-and-launch-description = Dorești să setezi „{ $name }” ca executabil și să îl rulezi?\nset-and-launch = Setează și rulează\n\n## Metadata Dialog\n\nopen-with = Deschide cu\nowner = Proprietar\ngroup = Grup\nother = Altele\n\n### Mode 0\n\nnone = Niciuna\n\n### Mode 1 (unusual)\n\nexecute-only = Doar executare\n\n### Mode 2 (unusual)\n\nwrite-only = Doar scriere\n\n### Mode 3 (unusual)\n\nwrite-execute = Scriere și executare\n\n### Mode 4\n\nread-only = Doar citire\n\n### Mode 5\n\nread-execute = Citire și executare\n\n### Mode 6\n\nread-write = Citire și scriere\n\n### Mode 7\n\nread-write-execute = Citire, scriere și executare\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Eroare la deschiderea directorului\nfavorite-path-error-description =\n    Nu s-a putut deschide „{ $path }”.\n    Este posibil să nu existe sau să nu ai permisiuni de acces.\n\n    Vrei să-l elimini din bara laterală?\nremove = Elimină\nkeep = Păstrează\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Adaugă o unitate de rețea\nconnect = Conectează\nconnect-anonymously = Conectare anonimă\nconnecting = Se conectează...\ndomain = Domeniu\nenter-server-address = Introdu adresa serverului\nnetwork-drive-description =\n    Adresele serverului includ prefixul protocolului și adresa.\n    Exemple: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Protocoale disponibile,Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// sau ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// sau ssh://\n    WebDav,dav:// sau davs://\nnetwork-drive-error = Nu se poate accesa unitatea de rețea\npassword = Parolă\nremember-password = Ține minte parola\ntry-again = Încearcă din nou\nusername = Nume utilizator\n\n## Operations\n\ncancelled = Anulat\nedit-history = Editează istoricul\nhistory = Istoric\nno-history = Nicio intrare în istoric.\npending = În așteptare\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, anulat\nprogress-paused = { $percent }%, întrerupt\nfailed = Eșuat\ncomplete = Complet\ncompressing =\n    Se comprimă { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }” ({ $progress })...\ncompressed =\n    Comprimat { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }”\ncopy_noun = Copiere\ncreating = Se creează „{ $name }” în „{ $parent }”\ncreated = S-a creat „{ $name }” în „{ $parent }”\ncopying =\n    Se copiază { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }” ({ $progress })...\ncopied =\n    Copiat { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }”\ndeleting =\n    Se șterge { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din { trash } ({ $progress })...\ndeleted =\n    Șters { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din { trash }\nemptying-trash = Se golește { trash } ({ $progress })...\nemptied-trash = Coșul { trash } a fost golit\nextracting =\n    Se extrage { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }” ({ $progress })...\nextracted =\n    Extras { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }”\nsetting-executable-and-launching = Se setează „{ $name }” ca executabil și se rulează\nset-executable-and-launched = „{ $name }” setat ca executabil și rulat\nmoving =\n    Se mută { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }” ({ $progress })...\nmoved =\n    Mutat { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din „{ $from }” în „{ $to }”\nrenaming = Se redenumește „{ $from }” în „{ $to }”\nrenamed = S-a redenumit „{ $from }” în „{ $to }”\nrestoring =\n    Se restabilește { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din { trash } ({ $progress })...\nrestored =\n    Restabilit { $items } { $items ->\n        [one] element\n       *[other] elemente\n    } din { trash }\nunknown-folder = dosar necunoscut\n\n## Open with\n\nmenu-open-with = Deschide cu...\ndefault-app = { $name } (implicit)\n\n## Show details\n\nshow-details = Afișează detalii\ntype = Tip: { $mime }\nitems = Elemente: { $items }\nitem-size = Dimensiune: { $size }\nitem-created = Creat: { $created }\nitem-modified = Modificat: { $modified }\nitem-accessed = Accesat: { $accessed }\ncalculating = Se calculează...\n\n## Settings\n\nsettings = Setări\nsingle-click = Un singur clic pentru deschidere\n\n### Appearance\n\nappearance = Aspect\ntheme = Temă\nmatch-desktop = Potrivește cu desktopul\ndark = Întunecat\nlight = Deschis\n\n### Type to Search\n\ntype-to-search = Tastează pentru a căuta\ntype-to-search-recursive = Caută în dosarul curent și subdosare\ntype-to-search-enter-path = Introduce calea către dosar sau fișier\n# Context menu\nadd-to-sidebar = Adaugă în bara laterală\ncompress = Comprimă\ndelete-permanently = Șterge definitiv\nextract-here = Extrage aici\nnew-file = Fișier nou...\nnew-folder = Dosar nou...\nopen-in-terminal = Deschide în terminal\nmove-to-trash = Mută în coș\nrestore-from-trash = Recuperează din coș\nremove-from-sidebar = Elimină din bara laterală\nsort-by-name = Sortează după nume\nsort-by-modified = Sortează după modificare\nsort-by-size = Sortează după dimensiune\nsort-by-trashed = Sortează după dată ștergere\n\n## Desktop\n\nchange-wallpaper = Schimbă fundalul...\ndesktop-appearance = Aspect desktop...\ndisplay-settings = Setări ecran...\n\n# Menu\n\n\n## File\n\nfile = Fișier\nnew-tab = Filă nouă\nnew-window = Fereastră nouă\nrename = Redenumește...\nclose-tab = Închide fila\nquit = Închide aplicația\n\n## Edit\n\nedit = Editare\ncut = Taie\ncopy = Copiază\npaste = Lipește\nselect-all = Selectează tot\n\n## View\n\nzoom-in = Mărește\ndefault-size = Dimensiune implicită\nzoom-out = Micșorează\nview = Vizualizare\ngrid-view = Vizualizare grilă\nlist-view = Vizualizare listă\nshow-hidden-files = Afișează fișiere ascunse\nlist-directories-first = Listează directoarele primele\ngallery-preview = Previzualizare galerie\nmenu-settings = Setări...\nmenu-about = Despre Fișierele COSMIC...\n\n## Sort\n\nsort = Sortare\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Cele mai noi primele\nsort-oldest-first = Cele mai vechi primele\nsort-smallest-to-largest = De la mic la mare\nsort-largest-to-smallest = De la mare la mic\n"
  },
  {
    "path": "i18n/ru/cosmic_files.ftl",
    "content": "cosmic-files = Файлы\nempty-folder = Папка пуста\nempty-folder-hidden = Папка пуста (есть скрытые элементы)\nno-results = Ничего не найдено\nfilesystem = Файловая система\nhome = Домашняя папка\ntrash = Корзина\nnetworks = Сеть\nnotification-in-progress = Выполняются файловые операции\nrecents = Недавние документы\nundo = Отменить\ntoday = Сегодня\n# List view\nname = Имя\nmodified = Изменено\ntrashed-on = Удалено\nsize = Размер\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Создать архив\n\n## Empty Trash Dialog\n\nempty-trash = Очистить корзину\nempty-trash-warning = Элементы в папке «Корзина» будут удалены без возможности восстановления\n# New File/Folder Dialog\ncreate-new-file = Создать новый файл\ncreate-new-folder = Создать новую папку\nfile-name = Имя файла\nfolder-name = Имя папки\nfile-already-exists = Файл с таким именем уже существует.\nfolder-already-exists = Папка с таким именем уже существует.\nname-hidden = Имена, начинающиеся на «.», будут скрыты.\nname-invalid = Имя не может быть «{ $filename }».\nname-no-slashes = Имя не должно содержать «/».\n# Open/Save Dialog\ncancel = Отмена\nopen = Открыть\nopen-file = Открыть файл\nopen-folder = Открыть папку\nopen-in-new-tab = Открыть в новой вкладке\nopen-in-new-window = Открыть в новом окне\nopen-item-location = Открыть расположение элемента\nopen-multiple-files = Открыть несколько файлов\nopen-multiple-folders = Открыть несколько папок\nsave = Сохранить\nsave-file = Сохранить файл\n# Rename Dialog\nrename-file = Переименовать файл\nrename-folder = Переименовать папку\n# Replace Dialog\nreplace = Заменить\nreplace-title = { $filename } уже существует в данном каталоге.\nreplace-warning = Вы хотите заменить этот файл на тот, что сохраняете? Замена перезапишет все данные файла.\nreplace-warning-operation = Хотите заменить? Замена приведёт к перезаписи содержимого.\noriginal-file = Оригинальный файл\nreplace-with = Заменить на\napply-to-all = Применить ко всем\nkeep-both = Сохранить оба\nskip = Пропустить\n\n## Metadata Dialog\n\nowner = Владелец\ngroup = Группа\nother = Прочие\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Добавить сетевой диск\nconnect = Подключиться\nconnect-anonymously = Подключиться анонимно\nconnecting = Подключение…\ndomain = Домен\nenter-server-address = Введите адрес сервера\nnetwork-drive-description =\n    Адреса серверов включают префикс протокола и адрес.\n    Пример: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Доступные протоколы,Префикс\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// или ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// или ssh://\n    WebDav,dav:// или davs://\nnetwork-drive-error = Невозможно получить доступ к сетевому диску\npassword = Пароль\nremember-password = Запомнить пароль\ntry-again = Попробовать ещё раз\nusername = Имя пользователя\n\n## Operations\n\nedit-history = История редактирования\nhistory = История\nno-history = В истории нет записей.\npending = В процессе\nfailed = Не удалась\ncomplete = Завершена\ncompressing =\n    Сжатие { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из «{ $from }» в «{ $to }» ({ $progress })…\ncompressed =\n    Сжато { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из «{ $from }» в «{ $to }»\ncopy_noun = Копирование\ncreating = Создание { $name } в { $parent }\ncreated = Создан { $name } в { $parent }\ncopying =\n    Копирование { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из «{ $from }» в «{ $to }» ({ $progress })…\ncopied =\n    Скопировано { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из «{ $from }» в «{ $to }»\nemptying-trash = Очистка { trash } ({ $progress })…\nemptied-trash = { trash } очищена\nextracting =\n    Извлечение { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из «{ $from }» в «{ $to }» ({ $progress })…\nextracted =\n    Извлечено { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из «{ $from }» в «{ $to }»\nmoving =\n    Перемещение { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из «{ $from }» в «{ $to }» ({ $progress })…\nmoved =\n    Перемещено { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из «{ $from }» в «{ $to }»\nrenaming = Переименование «{ $from }» в «{ $to }»\nrenamed = «{ $from }» переименован в «{ $to }»\nrestoring =\n    Восстановление { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из { trash } ({ $progress })…\nrestored =\n    Восстановлено { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из { trash }\nunknown-folder = неизвестная папка\n\n## Open with\n\nmenu-open-with = Открыть с помощью…\ndefault-app = { $name } (по умолчанию)\n\n## Show details\n\nshow-details = Показать подробности\n\n## Properties\n\n\n## Settings\n\nsettings = Параметры\n\n### Appearance\n\nappearance = Оформление\ntheme = Тема\nmatch-desktop = Как в системе\ndark = Тёмная\nlight = Светлая\n# Context menu\nadd-to-sidebar = Добавить на боковую панель\ncompress = Сжать...\nextract-here = Распаковать\nnew-file = Новый файл…\nnew-folder = Новая папка…\nopen-in-terminal = Открыть в терминале\nmove-to-trash = Переместить в корзину\nrestore-from-trash = Восстановить из корзины\nremove-from-sidebar = Убрать с боковой панели\nsort-by-name = Сорт. по имени\nsort-by-modified = Сорт. по дате изменения\nsort-by-size = Сорт. по размеру\nsort-by-trashed = Сорт. по дате удаления\n\n# Menu\n\n\n## File\n\nfile = Файл\nnew-tab = Новая вкладка\nnew-window = Новое окно\nrename = Переименовать…\nclose-tab = Закрыть вкладку\nquit = Выйти\n\n## Edit\n\nedit = Правка\ncut = Вырезать\ncopy = Копировать\npaste = Вставить\nselect-all = Выбрать все\n\n## View\n\nzoom-in = Увеличить\ndefault-size = Размер по умолчанию\nzoom-out = Уменьшить\nview = Вид\ngrid-view = Сетка\nlist-view = Список\nshow-hidden-files = Показывать скрытые файлы\nlist-directories-first = Показывать сначала папки\nmenu-settings = Параметры...\nmenu-about = О приложении Файлы COSMIC...\n\n## Sort\n\nsort = Сортировка\nsort-a-z = От А до Я\nsort-z-a = От Я до А\nsort-newest-first = Сначала новые\nsort-oldest-first = Сначала старые\nsort-smallest-to-largest = От меньшего к большему\nsort-largest-to-smallest = От большего к меньшему\nsupport = Поддержка\nrepository = Репозиторий\ncancelled = Отменена\ndetails = Сведения\ndismiss = Скрыть сообщение\nremove = Убрать\ndesktop-view-options = Параметры вида рабочего стола…\nshow-on-desktop = Показывать на рабочем столе\ndesktop-folder-content = Содержимое папки рабочего стола\nmounted-drives = Подключённые диски\ntrash-folder-icon = Значок папки корзины\nicon-size-and-spacing = Размер и отступы значков\nicon-size = Размер значка\ngrid-spacing = Отступ по сетке\npause = Приостановить\nresume = Продолжить\nextract-password-required = Требуется пароль\nextract-to = Распаковать в…\nextract-to-title = Распаковать в папку\nmount-error = Не удалось получить доступ к диску\ncreate = Создать\nopen-with-title = Как вы хотите открыть «{ $name }»?\nbrowse-store = Искать в { $store }\nother-apps = Другие приложения\nrelated-apps = Связанные приложения\nselected-items = { $items } выделенных элем.\npermanently-delete-question = Навсегда удалить\ndelete = Удалить\npermanently-delete-warning = Вы уверены, что хотите навсегда удалить { $target }? Это действие необратимо.\nset-executable-and-launch = Сделать исполняемым и запустить\nset-executable-and-launch-description = Вы хотите сделать «{ $name }» исполняемым и запустить его?\nset-and-launch = Сделать и запустить\nopen-with = Открывать в\nnone = Нет прав\nexecute-only = Только исполнение\nwrite-only = Только запись\nwrite-execute = Запись и исполнение\nread-only = Только чтение\nread-execute = Чтение и исполнение\nread-write = Чтение и запись\nread-write-execute = Чтение, запись, исполнение\nfavorite-path-error = Не удалось открыть каталог\nfavorite-path-error-description =\n    Не удалось открыть «{ $path }».\n    Возможно, он не существует, либо у вас нет прав на его открытие.\n\n    Хотите убрать его с боковой панели?\nkeep = Оставить\nprogress = { $percent } %\nprogress-cancelled = { $percent } %, отменена\nprogress-failed = { $percent } %, не удалась\nprogress-paused = { $percent } %, приостановлена\nsetting-executable-and-launching = Установка «{ $name }» исполняемым и запуск\nset-executable-and-launched = «{ $name }» сделан исполнямым и запущен\nsetting-permissions = Изменение прав доступа «{ $name }» на { $mode }\nset-permissions = Права доступа «{ $name }» изменены на { $mode }\ntype = Тип: { $mime }\nitems = Элементов: { $items }\nitem-size = Размер: { $size }\nitem-created = Дата создания: { $created }\nitem-modified = Дата изменения: { $modified }\nitem-accessed = Дата доступа: { $accessed }\ncalculating = Вычисление…\nsingle-click = Открывать одним нажатием\ntype-to-search = Поле поиска\ntype-to-search-recursive = Поиск в текущей папке и подпапках\ntype-to-search-enter-path = Ввод пути к каталогу или файлу\ndelete-permanently = Удалить навсегда\neject = Извлечь\nremove-from-recents = Убрать из недавних\nchange-wallpaper = Изменить обои…\ndesktop-appearance = Параметры оформления…\ndisplay-settings = Параметры экрана…\nreload-folder = Обновить папку\ngallery-preview = Галерея предпросмотра\noperations-running =\n    { $running } { $running ->\n        [one] операция\n       *[other] опер.\n    } выполняется ({ $percent } %)…\noperations-running-finished =\n    { $running } { $running ->\n        [one] операция\n       *[other] опер.\n    } выполняется ({ $percent } %), { $finished } завершено…\ndeleting =\n    Удаление { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из { trash } ({ $progress })…\ndeleted =\n    Удалено { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из { trash }\npermanently-deleting =\n    Удаление навсегда { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    }\npermanently-deleted =\n    Удалено навсегда { $items } { $items ->\n        [one] эелмент\n       *[other] элем.\n    }\nremoving-from-recents =\n    Убирание { $items } { $items ->\n        [one] элемента\n       *[other] элем.\n    } из { recents }\nremoved-from-recents =\n    Убрано { $items } { $items ->\n        [one] элемент\n       *[other] элем.\n    } из { recents }\ntype-to-search-select = Выделение первого подходящего файла или папки\npasted-image = Вставленное изображение\npasted-text = Вставленный текст\npasted-video = Вставленное видео\ncopy-to-title = Выберите папку назначения\ncopy-to-button-label = Копировать\nmove-to-title = Выберите папку назначения\nmove-to-button-label = Переместить\ncopy-to = Копировать в...\nmove-to = Переместить в...\ncomment = Файловый менеджер для среды COSMIC\nkeywords = Папка;Менеджер;\nshow-recents = «Недавние документы» в бок. панели\nclear-recents-history = Очистить историю недавних\ncopy-path = Копировать путь\nmixed = Смешанные\n"
  },
  {
    "path": "i18n/sk/cosmic_files.ftl",
    "content": "cosmic-files = Súbory COSMIC\ncomment = Správca súborov pre prostredie COSMIC\nkeywords = Priečinok;Správca;Súbory;Manažér;Prehliadač;\nempty-folder = Priečinok je prázdny\nempty-folder-hidden = Priečinok je prázdny (obsahuje skryté položky)\nno-results = Neboli nájdené žiadne výsledky\nfilesystem = Súborový systém\nhome = Domov\nnetworks = Siete\nnotification-in-progress = Prebiehajú operácie so súbormi.\ntrash = Kôš\nrecents = Nedávne\nundo = Späť\ntoday = Dnes\n# Desktop view options\ndesktop-view-options = Možnosti zobrazenia pracovnej plochy...\nshow-on-desktop = Zobraziť na pracovnej ploche\ndesktop-folder-content = Obsah priečinka Pracovná plocha\nmounted-drives = Pripojené disky\ntrash-folder-icon = Ikona priečinka Kôš\nicon-size-and-spacing = Veľkosť ikon a rozostupy\nicon-size = Veľkosť ikon\ngrid-spacing = Rozostupy mriežky\n# List view\nname = Názov\nmodified = Upravené\ntrashed-on = Vymazané\nsize = Veľkosť\n# Progress footer\ndetails = Podrobnosti\ndismiss = Zavrieť správu\noperations-running =\n    { $running } { $running ->\n        [one] operácia\n        [few] operácie\n        [many] operácií\n       *[other] operácie\n    } prebieha ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] operácia\n        [few] operácie\n        [many] operácií\n       *[other] operácie\n    } prebieha ({ $percent }%), { $finished } dokončených...\npause = Pozastaviť\nresume = Pokračovať\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Vytvoriť archív\n\n## Extract Dialog\n\nextract-password-required = Vyžaduje sa heslo\nextract-to = Extrahovať do...\nextract-to-title = Extrahovať do priečinka\n\n## Empty Trash Dialog\n\nempty-trash = Vyprázdniť kôš\nempty-trash-warning = Naozaj chcete trvalo odstrániť všetky položky v koši?\n\n## Mount Error Dialog\n\nmount-error = Nie je možné získať prístup k disku\n\n## New File/Folder Dialog\n\ncreate-new-file = Vytvoriť nový súbor\ncreate-new-folder = Vytvoriť nový priečinok\nfile-name = Názov súboru\nfolder-name = Názov priečinka\nfile-already-exists = Súbor s týmto názvom už existuje.\nfolder-already-exists = Priečinok s týmto názvom už existuje.\nname-hidden = Názvy začínajúce bodkou budú skryté.\nname-invalid = Názov nemôže byť \"{ $filename }\".\nname-no-slashes = Názov nemôže obsahovať lomítka.\n\n## Open/Save Dialog\n\ncancel = Zrušiť\ncreate = Vytvoriť\nopen = Otvoriť\nopen-file = Otvoriť súbor\nopen-folder = Otvoriť priečinok\nopen-in-new-tab = Otvoriť v novej záložke\nopen-in-new-window = Otvoriť v novom okne\nopen-item-location = Otvoriť umiestnenie položky\nopen-multiple-files = Otvoriť viac súborov\nopen-multiple-folders = Otvoriť viac priečinkov\nsave = Uložiť\nsave-file = Uložiť súbor\n\n## Open With Dialog\n\nopen-with-title = Ako chcete otvoriť \"{ $name }\"?\nbrowse-store = Prehľadávať { $store }\nother-apps = Iné aplikácie\nrelated-apps = Súvisiace aplikácie\n\n## Permanently delete Dialog\n\nselected-items = { $items } vybraných položiek\npermanently-delete-question = Trvalo odstrániť\ndelete = Odstrániť\npermanently-delete-warning = Naozaj chcete trvalo odstrániť { $target }? Toto nie je možné vrátiť späť.\n\n## Rename Dialog\n\nrename-file = Premenovať súbor\nrename-folder = Premenovať priečinok\n\n## Replace Dialog\n\nreplace = Nahradiť\nreplace-title = \"{ $filename }\" už existuje v tomto umiestnení.\nreplace-warning = Chcete ho nahradiť tým, ktorý práve ukladáte? Nahradením sa prepíše jeho obsah.\nreplace-warning-operation = Chcete ho nahradiť? Nahradením sa prepíše jeho obsah.\noriginal-file = Pôvodný súbor\nreplace-with = Nahradiť s\napply-to-all = Použiť na všetky\nkeep-both = Ponechať oboje\nskip = Preskočiť\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Nastaviť ako spustiteľné a spustiť\nset-executable-and-launch-description = Chcete nastaviť \"{ $name }\" ako spustiteľné a spustiť ho?\nset-and-launch = Nastaviť a spustiť\n\n## Metadata Dialog\n\nopen-with = Otvoriť pomocou\nowner = Vlastník\ngroup = Skupina\nother = Ostatní\n\n### Mode 0\n\nnone = Žiadne\n\n### Mode 1 (unusual)\n\nexecute-only = Len spúšťanie\n\n### Mode 2 (unusual)\n\nwrite-only = Len zápis\n\n### Mode 3 (unusual)\n\nwrite-execute = Zápis a spúšťanie\n\n### Mode 4\n\nread-only = Len čítanie\n\n### Mode 5\n\nread-execute = Čítanie a spúšťanie\n\n### Mode 6\n\nread-write = Čítanie a zápis\n\n### Mode 7\n\nread-write-execute = Čítanie, zápis a spúšťanie\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = Chyba pri otváraní adresára\nfavorite-path-error-description =\n    Nepodarilo sa otvoriť \"{ $path }\".\n    Možno neexistuje alebo nemáte povolenie na jeho otvorenie.\n\n    Chcete ho odstrániť z bočného panela?\nremove = Odstrániť\nkeep = Ponechať\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Pridať sieťový disk\nconnect = Pripojiť\nconnect-anonymously = Pripojiť anonymne\nconnecting = Pripájanie...\ndomain = Doména\nenter-server-address = Zadajte adresu servera\nnetwork-drive-description =\n    Adresy serverov obsahujú prefix protokolu a adresu.\n    Príklady: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Dostupné protokoly,Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// alebo ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// alebo ssh://\n    WebDAV,dav:// alebo davs://\nnetwork-drive-error = Nie je možné získať prístup k sieťovému disku\npassword = Heslo\nremember-password = Zapamätať heslo\ntry-again = Skúsiť znova\nusername = Používateľské meno\n\n## Operations\n\ncancelled = Zrušené\nedit-history = Upraviť históriu\nhistory = História\nno-history = Žiadne položky v histórii.\npending = Čakajúce\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, zrušené\nprogress-paused = { $percent }%, pozastavené\nfailed = Zlyhalo\ncomplete = Dokončené\ncompressing =\n    Komprimujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\" ({ $progress })...\ncompressed =\n    Komprimované { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\"\ncopy_noun = Kopírovať\ncreating = Vytváram \"{ $name }\" v \"{ $parent }\"\ncreated = Vytvorené \"{ $name }\" v \"{ $parent }\"\ncopying =\n    Kopírujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\" ({ $progress })...\ncopied =\n    Skopírované { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\"\ndeleting =\n    Odstraňujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { trash } ({ $progress })...\ndeleted =\n    Odstránené { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { trash }\nemptying-trash = Vyprázdňujem { trash } ({ $progress })...\nemptied-trash = Kôš bol vyprázdnený\nextracting =\n    Extrahujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\" ({ $progress })...\nextracted =\n    Extrahované { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\"\nsetting-executable-and-launching = Nastavujem \"{ $name }\" ako spustiteľné a spúšťam\nset-executable-and-launched = \"{ $name }\" nastavené ako spustiteľné a spustené\nsetting-permissions = Nastavujem oprávnenia pre \"{ $name }\" na { $mode }\nset-permissions = Oprávnenia pre \"{ $name }\" nastavené na { $mode }\nmoving =\n    Presúvam { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\" ({ $progress })...\nmoved =\n    Presunuté { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z \"{ $from }\" do \"{ $to }\"\npermanently-deleting =\n    Trvalo odstraňujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    }\npermanently-deleted =\n    Trvalo odstránené { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    }\nremoving-from-recents =\n    Odstraňujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { recents }\nremoved-from-recents =\n    Odstránené { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { recents }\nrenaming = Premenovávam \"{ $from }\" na \"{ $to }\"\nrenamed = Premenované \"{ $from }\" na \"{ $to }\"\nrestoring =\n    Obnovujem { $items } { $items ->\n        [one] položku\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { trash } ({ $progress })...\nrestored =\n    Obnovené { $items } { $items ->\n        [one] položka\n        [few] položky\n        [many] položiek\n       *[other] položky\n    } z { trash }\nunknown-folder = neznámy priečinok\n\n## Open with\n\nmenu-open-with = Otvoriť pomocou...\ndefault-app = { $name } (predvolené)\n\n## Show details\n\nshow-details = Zobraziť podrobnosti\ntype = Typ: { $mime }\nitems = Položky: { $items }\nitem-size = Veľkosť: { $size }\nitem-created = Vytvorené: { $created }\nitem-modified = Upravené: { $modified }\nitem-accessed = Prístup: { $accessed }\ncalculating = Vypočítavam...\n\n## Settings\n\nsettings = Nastavenia\nsingle-click = Otvoriť jedným kliknutím\n\n### Appearance\n\nappearance = Vzhľad\ntheme = Téma\nmatch-desktop = Prispôsobiť pracovnej ploche\ndark = Tmavá\nlight = Svetlá\n\n### Type to Search\n\ntype-to-search = Hľadať písaním\ntype-to-search-recursive = Prehľadáva aktuálny priečinok a všetky podpriečinky\ntype-to-search-enter-path = Zadajte cestu k adresáru alebo súboru\n# Context menu\nadd-to-sidebar = Pridať do bočného panela\ncompress = Komprimovať\ndelete-permanently = Trvalo odstrániť\neject = Vysunúť\nextract-here = Extrahovať sem\nnew-file = Nový súbor...\nnew-folder = Nový priečinok...\nopen-in-terminal = Otvoriť v termináli\nmove-to-trash = Presunúť do koša\nrestore-from-trash = Obnoviť z koša\nremove-from-sidebar = Odstrániť z bočného panela\nsort-by-name = Zoradiť podľa názvu\nsort-by-modified = Zoradiť podľa úpravy\nsort-by-size = Zoradiť podľa veľkosti\nsort-by-trashed = Zoradiť podľa času odstránenia\nremove-from-recents = Odstrániť z nedávnych\n\n## Desktop\n\nchange-wallpaper = Zmeniť tapetu...\ndesktop-appearance = Vzhľad pracovnej plochy...\ndisplay-settings = Nastavenia zobrazenia...\n\n# Menu\n\n\n## File\n\nfile = Súbor\nnew-tab = Nová záložka\nnew-window = Nové okno\nreload-folder = Obnoviť priečinok\nrename = Premenovať...\nclose-tab = Zavrieť záložku\nquit = Ukončiť\n\n## Edit\n\nedit = Upraviť\ncut = Vystrihnúť\ncopy = Kopírovať\npaste = Prilepiť\nselect-all = Vybrať všetko\n\n## View\n\nzoom-in = Priblížiť\ndefault-size = Predvolená veľkosť\nzoom-out = Oddialiť\nview = Zobraziť\ngrid-view = Zobrazenie mriežky\nlist-view = Zobrazenie zoznamu\nshow-hidden-files = Zobraziť skryté súbory\nlist-directories-first = Najskôr priečinky\ngallery-preview = Náhľad galérie\nmenu-settings = Nastavenia...\nmenu-about = O aplikácii Súbory COSMIC...\n\n## Sort\n\nsort = Zoradiť\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Najnovšie najskôr\nsort-oldest-first = Najstaršie najskôr\nsort-smallest-to-largest = Od najmenších po najväčšie\nsort-largest-to-smallest = Od najväčších po najmenšie\nrepository = Repozitár\nsupport = Podpora\nprogress-failed = { $percent }%, zlyhalo\n"
  },
  {
    "path": "i18n/sl/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/sr/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/sr-Cyrl/cosmic_files.ftl",
    "content": "empty-folder = Празна фасцикла\nempty-folder-hidden = Празна фасцикла (има скривене ставке)\nfilesystem = Систем датотека\ntrash = Отпад\n\n# Context Pages\n\n\n## Properties\n\n\n## Settings\n\nsettings = Подешавања\n\n### Appearance\n\nappearance = Изглед\ntheme = Тема\nmatch-desktop = Као систем\ndark = Тамна\nlight = Светла\n# Context menu\nnew-file = Нова датотека\nnew-folder = Нова фасцикла\nmove-to-trash = Премести у отпад\nrestore-from-trash = Врати из отпада\n\n# Menu\n\n\n## File\n\nfile = Датотека\nnew-tab = Нова картица\nnew-window = Нови прозор\nclose-tab = Затвори картицу\nquit = Изађи\n\n## Edit\n\nedit = Уреди\ncut = Исеци\ncopy = Копирај\npaste = Налепи\nselect-all = Изабери све\n\n## View\n\nview = Приказ\ngrid-view = Прикажи мрежу\nlist-view = Прикажи списак\nmenu-settings = Подешавања...\ncosmic-files = COSMIC Фајлови\nopen-file = Отвори фајл\ncancel = Прекини\nrepository = Репозиторијум\nsupport = Подршка\nno-results = Није пронађен ниједан резултат\nhome = Кућа\nopen-folder = Отвори директоријум\npassword = Шифра\nnetworks = Мреже\nnotification-in-progress = Операције над фајловима су у току.\nskip = Прескочи\nrecents = Скорије\nundo = Поништи промену\ntoday = Данас\ndesktop-view-options = Опције изгледа радне површине...\nshow-on-desktop = Покажи на радној површини\ndesktop-folder-content = Садржај директоријума радне површине\nmounted-drives = Приључена складишта података\ntrash-folder-icon = Иконица корпе са отпаткама\nicon-size-and-spacing = Величина иконице и размак\nicon-size = Величина иконице\n"
  },
  {
    "path": "i18n/sr-Latn/cosmic_files.ftl",
    "content": "empty-folder = Prazna fascikla\nempty-folder-hidden = Prazna fascikla (ima skrivene stavke)\nfilesystem = Sistem datoteka\ntrash = Otpad\n\n# Context Pages\n\n\n## Properties\n\n\n## Settings\n\nsettings = Podešavanja\n\n### Appearance\n\nappearance = Izgled\ntheme = Tema\nmatch-desktop = Kao sistem\ndark = Tamna\nlight = Svetla\n# Context menu\nnew-file = Nova datoteka\nnew-folder = Nova fascikla\nmove-to-trash = Premesti u otpad\nrestore-from-trash = Vrati iz otpada\n\n# Menu\n\n\n## File\n\nfile = Datoteka\nnew-tab = Nova kartica\nnew-window = Novi prozor\nclose-tab = Zatvori karticu\nquit = Izađi\n\n## Edit\n\nedit = Uredi\ncut = Iseci\ncopy = Kopiraj\npaste = Nalepi\nselect-all = Izaberi sve\n\n## View\n\nview = Prikaz\ngrid-view = Prikaži mrežu\nlist-view = Prikaži spisak\nmenu-settings = Podešavanja...\nrepository = Repozitorijum\nsupport = Podrška\ncancel = Poništi\nzoom-in = Uvećaj\ndefault-size = Podrazumevana veličina\nzoom-out = Umanji\n"
  },
  {
    "path": "i18n/sv/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Filer\ncomment = Filhanterare för skrivbordsmiljön COSMIC\nkeywords = Folder;Katalog;Mapp;Manager;\nempty-folder = Mappen är tom\nempty-folder-hidden = Mappen är tom (har dolda objekt)\nno-results = Inga resultat hittades\nfilesystem = Filsystem\nhome = Hem\nnetworks = Nätverk\nnotification-in-progress = Filåtgärder pågår\ntrash = Papperskorg\nrecents = Senaste\nundo = Ångra\ntoday = Idag\n# Skrivbordsvyalternativ\ndesktop-view-options = Skrivbordsvyalternativ...\nshow-on-desktop = Visa på skrivbord\ndesktop-folder-content = Skrivbordsmappinnehåll\nmounted-drives = Monterade enheter\ntrash-folder-icon = Ikon för papperskorgen\nicon-size-and-spacing = Ikonstorlek och mellanrum\nicon-size = Ikonstorlek\n\n# Dialogruta\n\n\n# Dialogrutor\n\n\n## Komprimera dialogruta\n\ncreate-archive = Skapa arkiv\n\n## Töm papperskorgen dialogruta\n\nempty-trash = Töm papperskorgen\nempty-trash-warning = Objekt i papperskorgen kommer att tas bort permanent\n\n## Monteringsfel dialogruta\n\nmount-error = Kan inte komma åt enheten\n\n## Ny Fil/katalog dialogruta\n\ncreate-new-file = Skapa ny fil\ncreate-new-folder = Skapa ny mapp\nfile-name = Filnamn\nfolder-name = Mappnamn\nfile-already-exists = En fil med det namnet finns redan\nfolder-already-exists = En mapp med det namnet finns redan\nname-hidden = Namn som börjar med \".\" kommer att vara dolda\nname-invalid = Namnet får inte vara \"{ $filename }\"\nname-no-slashes = Namnet får inte innehålla snedstreck\n\n## Öppna/Spara dialogruta\n\ncancel = Avbryt\ncreate = Skapa\nopen = Öppna\nopen-file = Öppna fil\nopen-folder = Öppna mapp\nopen-in-new-tab = Öppna i en ny flik\nopen-in-new-window = Öppna i nytt fönster\nopen-item-location = Öppna objektets plats\nopen-multiple-files = Öppna flera filer\nopen-multiple-folders = Öppna flera mappar\nsave = Spara\nsave-file = Spara fil\n\n## Öppna med dialogruta\n\nopen-with-title = Hur vill du öppna \"{ $name }\"?\nbrowse-store = Bläddra i { $store }\n\n## Byt namn dialogruta\n\nrename-file = Byt namn på fil\nrename-folder = Byt namn på mapp\n\n## Ersätt dialogruta\n\nreplace = Byt ut\nreplace-title = \"{ $filename }\" finns redan på den här platsen\nreplace-warning = Vill du ersätta filen med den du sparar? Om du ersätter den kommer dess innehåll att skrivas över.\nreplace-warning-operation = Vill du ersätta den? Om du ersätter den kommer dess innehåll att skrivas över.\noriginal-file = Originalfil\nreplace-with = Ersätt med\napply-to-all = Verkställ för alla\nkeep-both = Behåll båda\nskip = Hoppa över\n\n## Ställ in som körbar och starta dialogruta\n\nset-executable-and-launch = Gör körbar och starta\nset-executable-and-launch-description = Vill du göra \"{ $name }\" körbar och starta den?\nset-and-launch = Ställ in och starta\n\n## Metadata dialogruta\n\nopen-with = Öppna med\nowner = Ägare\ngroup = Grupp\nother = Andra\nmixed = Blandade\n# Listvy\nname = Namn\nmodified = Ändrad\ntrashed-on = Kastad\nsize = Storlek\n# Framstegssidfot\ndetails = Detaljer\ndismiss = Avfärda meddelande\noperations-running =\n    { $running } { $running ->\n        [one] åtgärd\n       *[other] åtgärder\n    } kör ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] åtgärd\n       *[other] åtgärder\n    } kör ({ $percent }%), { $finished } slutförda...\npause = Pausa\nresume = Återuppta\n\n# Kontextsidor\n\n\n## Om\n\n\n## Lägg till en Nätverksenhet\n\nadd-network-drive = Lägg till en Nätverksenhet\nconnect = Anslut\nconnect-anonymously = Anslut anonymt\nconnecting = Ansluter...\ndomain = Domän\nenter-server-address = Ange serveradress\ntry-again = Försök igen\nusername = Användarnamn\nnetwork-drive-description =\n    Serveradresser består av ett protokollprefix och en adress.\n    Exempel: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Se till att behålla kommatecken som skiljer kolumnerna åt\n\nnetwork-drive-schemes =\n    Tillgängliga protokoll, Prefix\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// eller ftps://\n    Network File System (NFS),nfs://\n    Server Message Block (SMB),smb://\n    SSH-filöverföringsprotokoll,sftp:// eller ssh://\n    WebDav,dav:// eller davs://\nnetwork-drive-error = Kan inte komma åt nätverksenheten\npassword = Lösenord\nremember-password = Kom ihåg lösenord\n\n## Operationer\n\ncancelled = Avbruten\nedit-history = Redigera historik\nhistory = Historik\nno-history = Inga objekt i historiken.\npending = Väntar\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, avbruten\nprogress-paused = { $percent }%, pausad\nfailed = Misslyckad\ncomplete = Färdig\ncompressing =\n    Komprimerar { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från \"{ $from }\" till \"{ $to }\" ({ $progress })...\ncompressed =\n    Komprimerade { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från \"{ $from }\" till \"{ $to }\"\ncopy_noun = Kopiera\ncreating = Skapar \"{ $name }\" i \"{ $parent }\"\ncreated = Skapade \"{ $name }\" i \"{ $parent }\"\ncopying =\n    Kopierar { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\" ({ $progress })...\ncopied =\n    Kopierade { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\"\nemptying-trash = Tömmer { trash } ({ $progress })...\nemptied-trash = Tömde { trash }\nextracting =\n    Packar upp { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\" ({ $progress })...\nextracted =\n    Packade upp { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\"\nsetting-executable-and-launching = Gör \"{ $name }\" körbar och startar\nset-executable-and-launched = Gjorde \"{ $name }\" körbar och startade\nmoving =\n    Flyttar { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\" ({ $progress })...\nmoved =\n    Flyttade { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från \"{ $from }\" till \"{ $to }\"\nrenaming = Byter namn på \"{ $from }\" till \"{ $to }\"\nrenamed = Bytt namn på \"{ $from }\" till \"{ $to }\"\nrestoring =\n    Återställer { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från { trash } ({ $progress })...\nrestored =\n    Återställt { $items } { $items ->\n        [one] objekt\n       *[other] flera objekt\n    } från { trash }\nunknown-folder = okänd mapp\n\n## Öppna med\n\nmenu-open-with = Öppna med...\ndefault-app = { $name } (standard)\n\n## Visa detaljer\n\nshow-details = Visa detaljer\ntype = Typ: { $mime }\nitems = Objekt: { $items }\nitem-size = Storlek: { $size }\nitem-created = Skapad: { $created }\nitem-modified = Ändrad: { $modified }\nitem-accessed = Åtkomst: { $accessed }\ncalculating = Beräknar...\n\n## Egenskaper\n\n\n## Inställningar\n\nsettings = Inställningar\nsingle-click = Ett enkelklick för att öppna\n\n### Utseende\n\nappearance = Utseende\ntheme = Tema\nmatch-desktop = Matcha skrivbordet\ndark = Mörkt\nlight = Ljust\n\n### Skriv för att söka\n\ntype-to-search = Skriv för att söka\ntype-to-search-recursive = Söker i den aktuella mappen och alla undermappar\ntype-to-search-enter-path = Anger sökvägen till mappen eller filen\n# Kontext meny\nadd-to-sidebar = Lägg till i sidofält\ncompress = Komprimera...\nextract-here = Packa upp\nnew-file = Ny fil…\nnew-folder = Ny mapp…\nopen-in-terminal = Öppna i terminal\nmove-to-trash = Flytta till papperskorg\nrestore-from-trash = Återställ från papperskorgen\nremove-from-sidebar = Ta bort från sidofält\nsort-by-name = Sortera efter namn\nsort-by-modified = Sortera efter senast ändrad\nsort-by-size = Sortera efter storlek\nsort-by-trashed = Sortera efter borttagningstid\n\n## Skrivbord\n\nchange-wallpaper = Byt bakgrund...\ndesktop-appearance = Skrivbordsutseende...\ndisplay-settings = Skärminställningar...\n\n# Meny\n\n\n## Fil\n\nfile = Fil\nnew-tab = Ny flik\nnew-window = Nytt fönster\nrename = Byt namn...\nclose-tab = Stäng flik\nquit = Avsluta\n\n## Redigera\n\nedit = Redigera\ncut = Klipp ut\ncopy = Kopiera\npaste = Klistra in\nselect-all = Välj alla\n\n## Visa\n\nzoom-in = Zooma in\ndefault-size = Standardstorlek\nzoom-out = Zooma ut\nview = Visa\ngrid-view = Rutnätsvy\nlist-view = Listvy\nshow-hidden-files = Visa dolda filer\nlist-directories-first = Lista mappar först\ngallery-preview = Galleri förhandsvisning\nmenu-settings = Inställningar…\nmenu-about = Om COSMIC Filer...\n\n## Sortera\n\nsort = Sortera\nsort-a-z = A-Ö\nsort-z-a = Ö-A\nsort-newest-first = Nyaste först\nsort-oldest-first = Äldst först\nsort-smallest-to-largest = Minsta till största\nsort-largest-to-smallest = Största till minsta\nremove = Ta bort\nrepository = Källkod\nsupport = Support\ngrid-spacing = Rutnätsmellanrum\nextract-password-required = Lösenord krävs\nextract-to = Packa upp till...\nextract-to-title = Packa upp till mapp\nother-apps = Andra program\nrelated-apps = Relaterade program\npermanently-delete-question = Ta bort permanent?\ndelete = Ta bort\npermanently-delete-warning = { $target } kommer att tas bort permanent. Detta kan inte göras ogjort.\nnone = Ingen\nexecute-only = Endast exekvera\nwrite-only = Endast skriva\nwrite-execute = Skriva och exekvera\nread-only = Endast läsa\nread-execute = Läsa och exekvera\nread-write = Läsa och skriva\nread-write-execute = Läsa, skriva och exekvera\nfavorite-path-error = Fel vid öppning av mapp\nfavorite-path-error-description =\n    Kunde inte öppna \"{ $path }\"\n    \"{ $path }\" finns kanske inte eller så har du inte behörighet att öppna den\n\n    Vill du ta bort den från sidolisten?\nkeep = Behåll\nprogress-failed = { $percent }%, misslyckades\ndeleting =\n    Raderar { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från { trash } ({ $progress })...\ndeleted =\n    Borttagna { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från { trash }\nsetting-permissions = Sätter behörigheter för \"{ $name }\" till { $mode }\nset-permissions = Satte behörigheter för \"{ $name }\" to { $mode }\npermanently-deleting =\n    Raderar { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } permanent\npermanently-deleted =\n    Permanent borttagna { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    }\nremoving-from-recents =\n    Tar bort { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från { recents }\nremoved-from-recents =\n    Tog bort { $items } { $items ->\n        [one] objekt\n       *[other] objekt\n    } från { recents }\ndelete-permanently = Ta bort permanent\neject = Mata ut\nremove-from-recents = Ta bort från senaste\nreload-folder = Ladda om mapp\nselected-items = De { $items } valda objekten\nempty-trash-title = Töm papperskorgen?\ntype-to-search-select = Markerar den första matchande filen eller mappen\npasted-image = Inklistrad bild\npasted-text = Inklistrad text\npasted-video = Inklistrad video\ncopy-to-title = Välj mål för kopiering\ncopy-to-button-label = Kopiera\nmove-to-title = Välj mål för flytt\nmove-to-button-label = Flytta\ncopy-to = Kopiera till...\nmove-to = Flytta till...\nshow-recents = Mapp för senast använda filer i sidofältet\nclear-recents-history = Töm historik för Senaste\ncopy-path = Kopiera sökväg\n"
  },
  {
    "path": "i18n/ta/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/th/cosmic_files.ftl",
    "content": "cosmic-files = ตัวจัดการไฟล์ COSMIC\nempty-folder = แฟ้มเปล่า\nempty-folder-hidden = แฟ้มเปล่า (มีแฟ้มที่ซ่อนอยู่)\nno-results = ไม่พบผลลัพธ์\nfilesystem = ระบบไฟล์\nhome = บ้าน\nnetworks = เครือข่าย\nnotification-in-progress = กำลังดำเนินการไฟล์\ntrash = ถังขยะ\nrecents = ล่าสุด\nundo = เลิกทำ\ntoday = วันนี้\n# Desktop view options\ndesktop-view-options = ตัวเลือกมุมมองหน้าจอหลัก...\nshow-on-desktop = แสดงบนหน้าจอหลัก\ndesktop-folder-content = เนื้อหาแฟ้มหน้าจอหลัก\nmounted-drives = ไดร์ฟที่ใช้งานได้\ntrash-folder-icon = ไอคอนแฟ้มถังขยะ\nicon-size-and-spacing = ขนาดและระยะห่างไอคอน\nicon-size = ขนาดไอคอน\n# List view\nname = ชื่อ\nmodified = แก้ไขล่าสุด\ntrashed-on = ถูกทิ้ง\nsize = ขนาด\n# Progress footer\ndetails = รายละเอียด\ndismiss = ไม่สนใจข้อความ\noperations-running = การดำเนินการ { $running } running ({ $percent }%)...\noperations-running-finished = { $running } operations running ({ $percent }%), { $finished } finished...\npause = หยุด\nresume = ทำต่อ\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = สร้างไฟล์บีบอัด\n\n## Empty Trash Dialog\n\nempty-trash = ล้างถังขยะ\nempty-trash-warning = คุณแน่ใจหรือไม่ว่าคุณต้องการจะลบภายในถังขยะถาวร\n\n## Mount Error Dialog\n\nmount-error = ไม่สามารถเข้าถึงไดร์ฟได้\n\n## New File/Folder Dialog\n\ncreate-new-file = สร้างไฟล์ใหม่\ncreate-new-folder = สร้างแฟ้มใหม่\nfile-name = ชื่อไฟล์\nfolder-name = ชื่อแฟ้ม\nfile-already-exists = มีไฟล์ชื่อนี้อยู่แล้ว\nfolder-already-exists = มีแฟ้มชื่อนี้อยู่แล้ว\nname-hidden = ชื่อที่ขึ้นต้นด้วย \".\" จะถูกซ่อน\nname-invalid = ไม่สามารถตั้ง \"{ $filename }\" เป็นชื่อได้\nname-no-slashes = ชื่อไม่สามารถมีเครื่องหมายทับได้\n\n## Open/Save Dialog\n\ncancel = ยกเลิก\ncreate = สร้าง\nopen = เปิด\nopen-file = เปิดไฟล์\nopen-folder = เปิดแฟ้ม\nopen-in-new-tab = เปิดในแทบใหม่\nopen-in-new-window = เปิดในหน้าต่างใหม่\nopen-item-location = เปิดตำแหน่งของรายการ\nopen-multiple-files = เปิดหลายไฟล์\nopen-multiple-folders = เปิดหลายแฟ้ม\nsave = บันทึก\nsave-file = บักทึกไฟล์\n\n## Open With Dialog\n\nopen-with-title = คุณจะเปิดไฟล์ \"{ $name }\" อย่างไร\nbrowse-store = เรียกดูใน { $store }\n\n## Rename Dialog\n\nrename-file = เปลี่ยนชื่อไฟล์\nrename-folder = เปลี่ยนชื่อแฟ้ม\n\n## Replace Dialog\n\nreplace = แทนที่\nreplace-title = มี \"{ $filename }\" อยู่แล้วที่ตำแหน่งนี้\nreplace-warning = คุณต้องการจะแทนที่ไฟล์ด้วยไฟล์ที่คุณกำลังบันทึกอยู่หรือไม่ การแทนที่จะเขียนทับเนื้อหาเดิม\nreplace-warning-operation = คุณต้องการจะแทนที่ไฟล์หรือไม่ การแทนที่จะเขียนทับเนื้อหาเดิม\noriginal-file = ไฟล์ต้นฉบับ\nreplace-with = แทนที่ด้วย\napply-to-all = นำไปใช้กับทั้งหมด\nkeep-both = เก็บไว้ทั้งคู่\nskip = ข้าม\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = ตั้งเป็นไฟล์ที่สามารถรันได้และเปิด\nset-executable-and-launch-description = คุณต้องการที่จะตั้งไฟล์ \"{ $name }\" ให้สามารถรันได้และเปิดเลยหรือไม่\nset-and-launch = ตั้งและเปิด\n\n## Metadata Dialog\n\nowner = เจ้าของ\ngroup = กลุ่ม\nother = ผู้อื่น\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = เพิ่มไดรฟ์เครือข่าย\nconnect = เชื่อมต่อ\nconnect-anonymously = เชื่อมต่อแบบไม่ระบุตัวตน\nconnecting = กำลังเชื่อมต่อ...\ndomain = โดเมน\nenter-server-address = ใส่ที่อยู่เซิร์ฟเวอร์\nnetwork-drive-description =\n    ที่อยู่ของเซิร์ฟเวอร์ประกอบด้วยโปรโตคอลและที่อยู่\n    เช่น: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    โปรโตคอลที่ใช้งานได้,โปรโตคอล\n    AppleTalk,afp://\n    File Transfer Protocol,ftp:// or ftps://\n    Network File System,nfs://\n    Server Message Block,smb://\n    SSH File Transfer Protocol,sftp:// or ssh://\n    WebDav,dav:// or davs://\nnetwork-drive-error = ไม่สามารถเข้าถึงไดร์ฟเครือข่าย\npassword = รหัสผ่าน\nremember-password = จดจำรหัสผ่าน\ntry-again = ลองอีกครั้ง\nusername = ชื่อผู้ใช้\n\n## Operations\n\ncancelled = ยกเลิกแล้ว\nedit-history = แก้ไขประวัติ\nhistory = ประวัติ\nno-history = ไม่มีไฟล์ในประวัติ\npending = รอดำเนินการ\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, ยกเลิกแล้ว\nprogress-paused = { $percent }%, หยุดชั่วคราว\nfailed = ล้มเหลว\ncomplete = เสร็จสิ้น\ncompressing =\n    กำลังบีบอัด { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\" ({ $progress })...\ncompressed =\n    บีบอัด { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\"\ncopy_noun = คัดลอก\ncreating = กำลังสร้างไฟล์ \"{ $name }\" ใน \"{ $parent }\ncreated = สร้าง \"{ $name }\" ใน \"{ $parent }\" แล้ว\ncopying =\n    กำลังคำลอก { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\" ({ $progress })...\ncopied =\n    เสร็จสิ้นการคัดลอก { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\"\nemptying-trash = กำลังล้างถังขยะ { trash } ไฟล์ ({ $progress })...\nemptied-trash = ล้างถังขยะแล้ว { trash } ไฟล์\nextracting =\n    กำลังแตกไฟล์ { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\" ({ $progress })...\nextracted =\n    เสร็จสิ้นการแตกไฟล์ { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\"\nsetting-executable-and-launching = กำลังตั้งไฟล์ \"{ $name }\" ให้สามารถรันได้และเปิด\nset-executable-and-launched = ตั้งไฟล์ \"{ $name }\" ให้สามารถรันได้และเปิดแล้ว\nmoving =\n    กำลังย้ายไฟล์ { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\" ({ $progress })...\nmoved =\n    เสร็จสิ้นการย้ายไฟล์ { $items } { $items ->\n        [one] ไฟล์\n       *[other] ไฟล์\n    } จาก \"{ $from }\" สู่ \"{ $to }\"\nrenaming = กำลังเปลี่ยนชื่อจาก \"{ $from }\" เป็น \"{ $to }\"\nrenamed = Renamed \"{ $from }\" to \"{ $to }\"\nrestoring =\n    Restoring { $items } { $items ->\n        [one] item\n       *[other] items\n    } from { trash } ({ $progress })...\nrestored =\n    Restored { $items } { $items ->\n        [one] item\n       *[other] items\n    } from { trash }\nunknown-folder = แฟ้มที่ไม่รู้จัก\n\n## Open with\n\nmenu-open-with = เปิดด้วย...\ndefault-app = { $name } (ค่าเริ่มต้น)\n\n## Show details\n\nshow-details = แสดงรายละเอียด\ntype = ชนิด: { $mime }\nitems = ไฟล์: { $items }\nitem-size = ขนาดไฟล์: { $size }\nitem-created = สร้างเมื่อ: { $created }\nitem-modified = แก้ไขเมื่อ: { $modified }\nitem-accessed = เปิดใช้เมื่อ: { $accessed }\ncalculating = กำลังคำนวณ...\n\n## Settings\n\nsettings = การตั้งค่า\n\n### Appearance\n\nappearance = ลักษณะ\ntheme = ธีม\nmatch-desktop = ใช้ตามธีมหน้าจอหลัก\ndark = ธีมมืด\nlight = ธีมสว่าง\n# Context menu\nadd-to-sidebar = เพิ่มเข้าแถบด้านข้าง\ncompress = บีบอัด\nextract-here = แตกไฟล์\nnew-file = สร้างไฟล์...\nnew-folder = สร้างแฟ้ม...\nopen-in-terminal = เปิดในเทอร์มินัล\nmove-to-trash = ย้ายไปถังขยะ\nrestore-from-trash = เรียกคืนจากถังขยะ\nremove-from-sidebar = นำออกจากแถบด้านข้าง\nsort-by-name = เรียงตามชื่อ\nsort-by-modified = เรียงตามเวลาแก้ไขล่าสุด\nsort-by-size = เรียงตามขนาด\nsort-by-trashed = เรียงตามเวลาลบ\n\n## Desktop\n\nchange-wallpaper = เปลี่ยนภาพพื้นหลัง...\ndesktop-appearance = ลักษณะหน้าจอหลัก...\ndisplay-settings = การตั้งค่าหน้าจอแสดงผล...\n\n# Menu\n\n\n## File\n\nfile = ไฟล์\nnew-tab = แทบใหม่\nnew-window = หน้าต่างใหม่\nrename = เปลี่ยนชื่อ...\nclose-tab = ปิดแทบ\nquit = ออก\n\n## Edit\n\nedit = แก้ไข\ncut = ตัด\ncopy = คัดลอก\npaste = วาง\nselect-all = เลือกทั้งหมด\n\n## View\n\nzoom-in = ซูมเข้า\ndefault-size = ขนาดดั้งเดิม\nzoom-out = ซูมออก\nview = มุมมอง\ngrid-view = มุมมองแบบตาราง\nlist-view = มุมมองแบบรายการ\nshow-hidden-files = แสดงไฟล์ที่ซ่อนอยู่\nlist-directories-first = แสดงแฟ้มก่อนเสมอ\ngallery-preview = ตัวอย่างแบบแกลเลอรี่\nmenu-settings = การตั้งค่า...\nmenu-about = เกี่ยวกับตัวจัดการไฟล์ COSMIC...\n\n## Sort\n\nsort = เรียง\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = ไฟล์ใหม่ก่อน\nsort-oldest-first = ไฟล์เก่าก่อน\nsort-smallest-to-largest = ขนาดเล็กก่อน\nsort-largest-to-smallest = ขนาดใหญ่ก่อน\n"
  },
  {
    "path": "i18n/ti/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/tr/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC Dosyalar\nempty-folder = Boş klasör\nempty-folder-hidden = Boş klasör (gizli ögeler içerir)\nno-results = Sonuç bulunamadı\nfilesystem = Dosya sistemi\nhome = Ev\nnetworks = Ağlar\nnotification-in-progress = Dosya işlemi devam etmekte\ntrash = Çöp\nrecents = Son kullanılanlar\nundo = Geri al\ntoday = Bugün\n# Desktop view options\ndesktop-view-options = Masaüstü görünüm seçenekleri...\nshow-on-desktop = Masaüstünde göster\ndesktop-folder-content = Masaüstü klasörü içeriği\nmounted-drives = Bağlı sürücüler\ntrash-folder-icon = Çöp klasörü simgesi\nicon-size-and-spacing = Simge boyutu ve boşluğu\nicon-size = Simge boyutu\n# List view\nname = Ad\nmodified = Değiştirildi\ntrashed-on = Çöpe atılma\nsize = Boyut\n# Progress footer\ndetails = Detaylar\ndismiss = Mesajı kapat\noperations-running =\n    { $running } { $running ->\n        [one] işlem\n       *[other] işlem\n    } çalışıyor ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] işlem\n       *[other] işlem\n    } çalışıyor ({ $percent }%), { $finished } bitti...\npause = Duraklat\nresume = Devam et\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = Arşiv oluştur\n\n## Empty Trash Dialog\n\nempty-trash = Çöpü boşalt\nempty-trash-warning = Çöp Kutusu klasöründeki öğeler kalıcı olarak silinecektir\n\n## Mount Error Dialog\n\nmount-error = Sürücüye erişilemedi\n\n## New File/Folder Dialog\n\ncreate-new-file = Yeni dosya oluştur\ncreate-new-folder = Yeni klasör oluştur\nfile-name = Dosya adı\nfolder-name = Klasör adı\nfile-already-exists = Bu isimde bir dosya zaten var\nfolder-already-exists = Bu isimde bir klasör zaten var\nname-hidden = \".\" ile başlayan adlar gizlenecek\nname-invalid = Ad \"{ $filename }\" olamaz\nname-no-slashes = Ad eğik çizgi içeremez\n\n## Open/Save Dialog\n\ncancel = Vazgeç\ncreate = Oluştur\nopen = Aç\nopen-file = Dosya aç\nopen-folder = Klasör aç\nopen-in-new-tab = Yeni sekmede aç\nopen-in-new-window = Yeni pencerede aç\nopen-item-location = Öge konumunu aç\nopen-multiple-files = Birden fazla dosyayı aç\nopen-multiple-folders = Birden fazla klasörü aç\nsave = Kaydet\nsave-file = Dosyayı kaydet\n\n## Open With Dialog\n\nopen-with-title = \"{ $name }\" dosyasını nasıl açmak istersiniz?\nbrowse-store = { $store }'sını gezin\n\n## Rename Dialog\n\nrename-file = Dosyayı yeniden adlandır\nrename-folder = Klasörü yeniden adlandır\n\n## Replace Dialog\n\nreplace = Değiştir\nreplace-title = \"{ $filename }\" bu konumda zaten var\nreplace-warning = Kaydettiğiniz dosya ile değiştirmek istiyor musunuz? Değiştirme içeriğin üzerine yazacak.\nreplace-warning-operation = Değiştirmek istiyor musunuz? Değiştirme içeriğin üzerine yazacak.\noriginal-file = Orijinal dosya\nreplace-with = Bununla değiştir\napply-to-all = Tümüne uygula\nkeep-both = İkisini de sakla\nskip = Atla\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = Çalıştırılabilir olarak ayarla ve başlat\nset-executable-and-launch-description = \"{ $name }\" dosyasını çalıştırılabilir olarak ayarlayıp başlatmak istiyor musunuz?\nset-and-launch = Ayarla ve başlat\n\n## Metadata Dialog\n\nowner = Sahibi\ngroup = Grup\nother = Diğer\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = Ağ sürücüsü ekle\nconnect = Bağlan\nconnect-anonymously = Anonim olarak bağlan\nconnecting = Bağlanılıyor...\ndomain = Alan\nenter-server-address = Sunucu adresi girin\nnetwork-drive-description =\n    Sunucu adresleri protokol ön eki ve adres içerir.\n    Örneğin: ssh://192.168.0.1, ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    Kullanılabilir protokoller,Ön eki\n    AppleTalk,afp://\n    Dosya Transfer Protokolü,ftp:// veya ftps://\n    Ağ Dosya Sistemi,nfs://\n    Sunucu Mesaj Bloğu,smb://\n    SSH Dosya Transfer Protokolü,sftp:// veya ssh://\n    WebDav,dav:// veya davs://\nnetwork-drive-error = Ağ sürücüsüne erişilemiyor\npassword = Parola\nremember-password = Parolayı hatırla\ntry-again = Tekrar dene\nusername = Kullanıcı adı\n\n## Operations\n\ncancelled = İptal edildi\nedit-history = Geçmişi düzenle\nhistory = Geçmiş\nno-history = Geçmişte öge bulunmuyor.\npending = Devam ediyor\nprogress = %{ $percent }\nprogress-cancelled = %{ $percent }, iptal edildi\nprogress-paused = %{ $percent }, duraklatıldı\nfailed = Başarısız\ncomplete = Tamamlandı\ncompressing =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e sıkıştırılıyor ({ $progress })...\ncompressed =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e sıkıştırıldı\ncopy_noun = Kopyala\ncreating = \"{ $parent }\" de \"{ $name }\" oluşturuluyor\ncreated = \"{ $parent }\" de \"{ $name }\" oluşturuldu\ncopying =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e kopyalanıyor ({ $progress })...\ncopied =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\"den \"{ $to }\" e kopyalandı\nemptying-trash = { trash } boşaltılıyor ({ $progress })...\nemptied-trash = { trash } boşaltıldı\nextracting =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e çıkartılıyor ({ $progress })...\nextracted =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e çıkartıldı\nsetting-executable-and-launching = \"{ $name }\" çalıştırılabilir olarak ayarlanıp başlatılıyor\nset-executable-and-launched = \"{ $name }\" çalıştırılabilir olarak ayarlanıp başlatıldı\nmoving =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e taşınıyor ({ $progress })...\nmoved =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } \"{ $from }\" den \"{ $to }\" e taşındı\nrenaming = \"{ $from }\" adı \"{ $to }\" olarak değiştiriliyor\nrenamed = \"{ $from }\" adı \"{ $to }\" olarak değiştirildi\nrestoring =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { trash } den geri yükleniyor ({ $progress })...\nrestored =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { trash } den geri yüklendi\nunknown-folder = bilinmeyen klasör\n\n## Open with\n\nmenu-open-with = Birlikte aç...\ndefault-app = { $name } (varsayılan)\n\n## Show details\n\nshow-details = Detayları göster\ntype = Tür: { $mime }\nitems = Öge sayısı: { $items }\nitem-size = Boyut: { $size }\nitem-created = Oluşturulma: { $created }\nitem-modified = Düzenlenme: { $modified }\nitem-accessed = Erişilme: { $accessed }\ncalculating = Hesaplanıyor...\n\n## Settings\n\nsettings = Ayarlar\n\n### Appearance\n\nappearance = Görünüm\ntheme = Tema\nmatch-desktop = Masaüstü stilini takip et\ndark = Karanlık\nlight = Aydınlık\n# Context menu\nadd-to-sidebar = Kenar çubuğuna ekle\ncompress = Sıkıştır\nextract-here = Çıkar\nnew-file = Yeni dosya...\nnew-folder = Yeni klasör...\nopen-in-terminal = Uçbirimde aç\nmove-to-trash = Çöpe taşı\nrestore-from-trash = Çöpten geri yükle\nremove-from-sidebar = Kenar çubuğundan kaldır\nsort-by-name = Ada göre sırala\nsort-by-modified = Düzenlenme tarihine göre sırala\nsort-by-size = Boyuta göre sırala\nsort-by-trashed = Silme tarihine göre sırala\n\n## Desktop\n\nchange-wallpaper = Arka planı değiştir...\ndesktop-appearance = Masaüstü görünümü...\ndisplay-settings = Görüntü ayarları...\n\n# Menu\n\n\n## File\n\nfile = Dosya\nnew-tab = Yeni sekme\nnew-window = Yeni pencere\nrename = Yeniden adlandır...\nclose-tab = Sekmeyi kapat\nquit = Çıkış\n\n## Edit\n\nedit = Düzenle\ncut = Kes\ncopy = Kopyala\npaste = Yapıştır\nselect-all = Tümünü seç\n\n## View\n\nzoom-in = Yakınlaştır\ndefault-size = Varsayılan boyut\nzoom-out = Uzaklaştır\nview = Görünüm\ngrid-view = Tablo görünümü\nlist-view = Liste görünümü\nshow-hidden-files = Gizli dosyaları göster\nlist-directories-first = Önce dizinleri listele\ngallery-preview = Galeri ön izlemesi\nmenu-settings = Ayarlar...\nmenu-about = COSMIC Dosyalar hakkında...\n\n## Sort\n\nsort = Sırala\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = Önce en yeni\nsort-oldest-first = Önce en eski\nsort-smallest-to-largest = En küçükten en büyüğe\nsort-largest-to-smallest = En büyükten en küçüğe\nrepository = Depo\nsupport = Destek\nremove = Kaldır\ngrid-spacing = Izgara aralığı\nextract-password-required = Parola gerekli\nextract-to = Buraya Çıkar…\nextract-to-title = Klasöre çıkar\nother-apps = Diğer uygulamalar\nrelated-apps = İlgili uygulamalar\nselected-items = { $items } seçili öğeler\npermanently-delete-question = Kalıcı olarak silinsin mi?\ndelete = Sil\npermanently-delete-warning = { $target } kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\nopen-with = Birlikte aç\nnone = Yok\nexecute-only = Yalnızca çalıştırma\nwrite-only = Yalnızca yazma\nwrite-execute = Yazma ve çalıştırma\nread-only = Yanlızca okuma\nread-execute = Okuma ve çalıştırma\nread-write = Okuma ve yazma\nread-write-execute = Okuma, yazma ve çalıştırma\nfavorite-path-error = Dizin açılırken hata oluştu\nfavorite-path-error-description =\n    \"{ $path }\" açılamadı\n    \"{ $path }\" Dosya mevcut olmayabilir veya açmak için izniniz olmayabilir.\n\n    Bunu kenar çubuğundan kaldırmak ister misiniz?\nkeep = Tut\nprogress-failed = %{ $percent }, başarısız oldu\ndeleting =\n    Çöp kutusundan { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { trash } ({ $progress }) siliniyor...\ndeleted =\n    Çöp kutusundan { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { trash } silindi\nsetting-permissions = \"{ $name }\" için izinler { $mode } olarak ayarlanıyor\nset-permissions = \"{ $name }\" için izinleri { $mode } olarak ayarla\npermanently-deleting =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } kalıcı olarak siliniyor\npermanently-deleted =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } kalıcı olarak silindi\nremoving-from-recents =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { recents } den kaldırılıyor\nremoved-from-recents =\n    { $items } { $items ->\n        [one] öge\n       *[other] öge\n    } { recents } den kaldırıldı\nsingle-click = Açmak için tek tıklama\ntype-to-search = Aramak için yazın\ntype-to-search-recursive = Geçerli klasörü ve tüm alt klasörleri arar\ntype-to-search-enter-path = Dizin veya dosyanın yolunu girer\ndelete-permanently = Kalıcı olarak sil\neject = Çıkart\nremove-from-recents = Son kullanılanlardan kaldır\nreload-folder = Klasörü yeniden yükle\ncomment = COSMIC masaüstü için dosya yöneticisi\nkeywords = Klasör;Yönetici;\nempty-trash-title = Çöp boşaltılsın mı?\ncopy-to-title = Kopyalama hedefini seçin\ncopy-to-button-label = Kopyala\nmove-to-title = Taşıma hedefini seçin\nmove-to-button-label = Taşı\npasted-image = Yapıştırılan Görüntü\npasted-text = Yapıştırılan Metin\npasted-video = Yapıştırılan Video\nclear-recents-history = Son kullanılanlar geçmişini temizle\ncopy-path = Yolu kopyala\ntype-to-search-select = İlk eşleşen dosyayı veya klasörü seçer\ncopy-to = Buraya kopyala…\nmove-to = Buraya taşı…\nshow-recents = Kenar çubuğundaki son kullanılanlar klasörü\n"
  },
  {
    "path": "i18n/uk/cosmic_files.ftl",
    "content": "cosmic-files = Файли COSMIC\nempty-folder = Порожня тека\nempty-folder-hidden = Порожня тека (містить приховані елементи)\nfilesystem = Файлова система\nhome = Домівка\ntrash = Смітник\nrecents = Нещодавні\nundo = Відмінити\n# List view\nname = Назва\nmodified = Змінено\nsize = Розмір\n\n# Dialogs\n\n\n## Empty Trash Dialog\n\nempty-trash = Спорожнити смітник\nempty-trash-warning = Елементи зі смітника будуть остаточно видалені\n\n## New File/Folder Dialog\n\ncreate-new-file = Створити новий файл\ncreate-new-folder = Створити нову теку\nfile-name = Назва файлу\nfolder-name = Назва теки\nfile-already-exists = Файл з такою назвою вже існує\nfolder-already-exists = Тека з такою назвою вже існує\nname-hidden = Назви, що починаються з «.», приховано\nname-invalid = Назва не може бути «{ $filename }»\nname-no-slashes = Назва не може містити скісні риски\n\n## Open/Save Dialog\n\ncancel = Скасувати\nopen = Відкрити\nopen-file = Відкрити файл\nopen-folder = Відкрити теку\nopen-in-new-tab = Відкрити в новій вкладці\nopen-in-new-window = Відкрити в новому вікні\nopen-multiple-files = Відкрити кілька файлів\nopen-multiple-folders = Відкрити кілька тек\nsave = Зберегти\nsave-file = Зберегти файл\n\n## Rename Dialog\n\nrename-file = Перейменувати файл\nrename-folder = Перейменувати теку\n\n## Replace Dialog\n\nreplace = Замінити\nreplace-title = «{ $filename }» вже існує в цій теці\nreplace-warning = Бажаєте замінити файл тим, що зберігаєте? Після заміни його вміст буде перезаписано.\nreplace-warning-operation = Бажаєте замінити його? Це перезапише його вміст.\noriginal-file = Початковий файл\nreplace-with = Замінити на\napply-to-all = Застосувати до всіх\nkeep-both = Залишити обидва\nskip = Пропустити\n\n# Context Pages\n\n\n## About\n\n\n## Operations\n\nedit-history = Історія дій\nhistory = Історія\nno-history = Історія порожня.\npending = Очікується\nfailed = Не виконано\ncomplete = Завершено\ncopy_noun = Копіювати\ncreating = Створення «{ $name }» в «{ $parent }»\ncreated = Створено «{ $name }» в «{ $parent }»\ncopying =\n    Копіювання { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з «{ $from }» в «{ $to }» ({ $progress })...\ncopied =\n    Скопійовано { $items } { $items ->\n        [one] елемент\n       *[other] елеменів\n    } з «{ $from }» в «{ $to }»\nemptying-trash = Спорожнення { trash } ({ $progress })...\nemptied-trash = Спорожнено { trash }\nmoving =\n    Переміщення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з «{ $from }» в «{ $to }» ({ $progress })...\nmoved =\n    Переміщено { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з «{ $from }» в «{ $to }»\nrenaming = Перейменування «{ $from }» на «{ $to }»\nrenamed = Перейменовано «{ $from }» на «{ $to }»\nrestoring =\n    Відновлення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з { trash } ({ $progress })...\nrestored =\n    Відновлено { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з { trash }\nunknown-folder = невідома тека\n\n## Open with\n\nmenu-open-with = Відкрити за допомогою...\ndefault-app = { $name } (звичайний)\n\n## Properties\n\n\n## Settings\n\nsettings = Налаштування\n\n### Appearance\n\nappearance = Вигляд\ntheme = Тема\nmatch-desktop = Системна\ndark = Темна\nlight = Світла\n# Context menu\nadd-to-sidebar = Додати до бічної панелі\nnew-file = Новий файл...\nnew-folder = Нова тека...\nopen-in-terminal = Відкрити у терміналі\nmove-to-trash = Пересунути до смітника\nrestore-from-trash = Відновити зі смітника\nremove-from-sidebar = Вилучити з бічної панелі\nsort-by-name = Упорядкувати за назвою\nsort-by-modified = Упорядкувати за зміною\nsort-by-size = Упорядкувати за розміром\n\n# Menu\n\n\n## File\n\nfile = Файл\nnew-tab = Нова вкладка\nnew-window = Нове вікно\nrename = Перейменувати...\nclose-tab = Закрити вкладку\nquit = Вийти\n\n## Edit\n\nedit = Редагувати\ncut = Вирізати\ncopy = Копіювати\npaste = Вставити\nselect-all = Вибрати все\n\n## View\n\nzoom-in = Збільшити\ndefault-size = Стандартний розмір\nzoom-out = Зменшити\nview = Вид\ngrid-view = Перегляд таблицею\nlist-view = Перегляд списком\nshow-hidden-files = Показати приховані файли\nlist-directories-first = Теки спочатку\nmenu-settings = Налаштування...\nmenu-about = Про Файли COSMIC...\nrepository = Сховище\nsupport = Підтримка\ndetails = Деталі\ndismiss = Закрити повідомлення\nremove = Вилучити\ncancelled = Скасовані\nno-results = Нічого не знайдено\nnetworks = Мережі\nnotification-in-progress = Триває обробка файлів\ntoday = Сьогодні\ndesktop-view-options = Параметри вигляду стільниці...\nshow-on-desktop = Показувати на стільниці\ndesktop-folder-content = Вміст теки Стільниця\nmounted-drives = Змонтовані диски\ntrash-folder-icon = Піктограма Смітника\nicon-size-and-spacing = Розмір піктограм і відстань між ними\nicon-size = Розмір піктограм\ngrid-spacing = Відстань між піктограмами\ntrashed-on = У смітнику\noperations-running =\n    { $running } { $running ->\n        [one] операція\n       *[other] операції\n    } виконується ({ $percent }%)...\noperations-running-finished =\n    { $running } { $running ->\n        [one] операція\n       *[other] операціі\n    } виконується ({ $percent }%), { $finished } завершено...\npause = Призупинити\nresume = Продовжити\ncreate-archive = Створити архів\nextract-password-required = Потрібен пароль\nextract-to = Видобути до...\nextract-to-title = Видобути до теки\nmount-error = Доступ до диска відсутній\ncreate = Створити\nopen-item-location = Відкрити розташування елемента\nopen-with-title = Як ви бажаєте відкрити «{ $name }»?\nbrowse-store = Переглянути { $store }\nother-apps = Інші застосунки\nrelated-apps = Пов'язані застосунки\npermanently-delete-question = Остаточно видалити?\ndelete = Видалити\npermanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати.\nset-executable-and-launch = Зробити виконуваним і запустити\nset-executable-and-launch-description = Бажаєте зробити \"{ $name }\" виконуваним і запустити його?\nset-and-launch = Зробити і запустити\nopen-with = Відкрити за допомогою\nowner = Власник\ngroup = Група\nother = Інші\nnone = Немає прав\nexecute-only = Тільки виконання\nwrite-only = Тільки запис\nwrite-execute = Запис і виконання\nread-only = Тільки перегляд\nread-execute = Перегляд і виконання\nread-write = Перегляд і запис\nread-write-execute = Перегляд, запис і виконання\nfavorite-path-error = Помилка при відкритті каталогу\nfavorite-path-error-description =\n    Не вдалося відкрити «{ $path }»\n    «{ $path }» можливо не існує або у вас немає прав на відкриття\n\n    Вилучити з бічної панелі?\nkeep = Залишити\nadd-network-drive = Додати мережевий диск\nconnect = З'єднати\nconnect-anonymously = З'єднатись анонімно\nconnecting = З'єднання…\ndomain = Домен\nenter-server-address = Введіть адресу сервера\nnetwork-drive-description =\n    Серверні адреси містять префікс протоколу і саму адресу.\n    Наприклад: ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    Доступні протоколи,Префікс\n    AppleTalk,afp://\n    Протокол Передавання Файлів,ftp:// або ftps://\n    Мережева Файлова Система,nfs://\n    Серверний Блок Повідомлень,smb://\n    Протокол Передавання Файлів SSH,sftp:// або ssh://\n    WebDAV,dav:// або davs://\nnetwork-drive-error = Доступ до мережевого диска відсутній\npassword = Пароль\nremember-password = Запам'ятати пароль\ntry-again = Спробувати знову\nusername = Ім'я користувача\nprogress = { $percent }%\nprogress-cancelled = { $percent }%, скасовано\nprogress-failed = { $percent }%, не вдалося\nprogress-paused = { $percent }%, призупинено\ncompressing =\n    Стиснення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з \"{ $from }\" до \"{ $to }\" ({ $progress })...\ncompressed =\n    Стиснуто { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з \"{ $from }\" до \"{ $to }\"\ndeleting =\n    Видалення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з { trash } ({ $progress })...\ndeleted =\n    Видалено { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з { trash }\nextracting =\n    Видобування { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з «{ $from }» в «{ $to }» ({ $progress })...\nextracted =\n    Видобуто { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з «{ $from }» в «{ $to }»\nsetting-executable-and-launching = Надання «{ $name }» прав на виконання та запуск\nset-executable-and-launched = «{ $name }» надано права на виконання і відкрито\nselected-items = Вибрані { $items } елементи\nsetting-permissions = Надання прав { $mode } для «{ $name }»\nset-permissions = Надано права { $mode } для «{ $name }»\nshow-details = Показати подробиці\ntype = Тип: { $mime }\nitems = Елементів: { $items }\nitem-size = Розмір: { $size }\nitem-created = Створено: { $created }\nitem-modified = Змінено: { $modified }\nitem-accessed = Дата доступу: { $accessed }\ncalculating = Обчислення...\nsingle-click = Відкривати одним клацанням\ntype-to-search = Введіть для пошуку\ntype-to-search-recursive = Шукає у поточній теці та всіх підтеках\ntype-to-search-enter-path = Вводить шлях до каталогу або файлу\ncompress = Стиснути...\ndelete-permanently = Остаточно видалити\neject = Безпечно вилучити\nextract-here = Видобути\nsort-by-trashed = Упорядкувати за часом видалення\nremove-from-recents = Вилучити з нещодавніх\nchange-wallpaper = Змінити зображення тла...\ndesktop-appearance = Вигляд стільниці...\ndisplay-settings = Налаштування дисплея...\nreload-folder = Оновити теку\ngallery-preview = Попередній перегляд галереї\nsort = Упорядкувати\nsort-a-z = А-Я\nsort-z-a = Я-А\nsort-newest-first = Спочатку найновіші\nsort-oldest-first = Спочатку найстаріші\nsort-smallest-to-largest = Від найменшого до найбільшого\nsort-largest-to-smallest = Від найбільшого до найменшого\npermanently-deleting =\n    Остаточне вилучення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    }\npermanently-deleted =\n    Остаточно вилучено { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    }\nremoving-from-recents =\n    Вилучення { $items } { $items ->\n        [one] елемента\n       *[other] елементів\n    } з { recents }\nremoved-from-recents =\n    Вилучено { $items } { $items ->\n        [one] елемент\n       *[other] елементи\n    } з { recents }\nempty-trash-title = Спорожити смітник?\ntype-to-search-select = Вибирає перший відповідний файл або папку\npasted-image = Вставлене Зображення\npasted-text = Вставлений Текст\npasted-video = Вставлене Видиво\ncopy-to-button-label = Копіювати\nmove-to-button-label = Перемістити\ncopy-to = Копіювати до…\nmove-to = Перемістити до…\ncopy-to-title = Виберіть місце призначення\nmove-to-title = Виберіть місце призначення\ncomment = Менеджер файлів для середовища COSMIC\nkeywords = Тека;Папка;Провідник;Менеджер;Каталог;\nshow-recents = Тека «Нещодавні» на бічній панелі\ncopy-path = Копіювати шлях\nclear-recents-history = Очистити нещодавні\n"
  },
  {
    "path": "i18n/uz/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/vi/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/yue-Hant/cosmic_files.ftl",
    "content": ""
  },
  {
    "path": "i18n/zh-CN/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC 文件管理器\nempty-folder = 空文件夹\nempty-folder-hidden = 空文件夹（包含隐藏项目）\nno-results = 未找到结果\nfilesystem = 文件系统\nhome = 主页\nnetworks = 网络\nnotification-in-progress = 文件操作正在进行中\ntrash = 回收站\nrecents = 最近访问\nundo = 撤销\ntoday = 今天\n# Desktop view options\ndesktop-view-options = 桌面视图选项…\nshow-on-desktop = 在桌面显示\ndesktop-folder-content = 桌面文件夹内容\nmounted-drives = 已装载驱动器\ntrash-folder-icon = 回收站图标\nicon-size-and-spacing = 图标大小与间距\nicon-size = 图标大小\ngrid-spacing = 网格间距\n# List view\nname = 名称\nmodified = 修改时间\ntrashed-on = 删除时间\nsize = 大小\n# Progress footer\ndetails = 详细信息\ndismiss = 清除消息\noperations-running =\n    正在进行 { $running } { $running ->\n        [one] 个操作\n       *[other] 个操作\n    }（{ $percent }%）…\noperations-running-finished =\n    正在进行 { $running } { $running ->\n        [one] 个操作\n       *[other] 个操作\n    }（{ $percent }%），{ $finished } 个操作已完成…\npause = 暂停\nresume = 继续\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = 创建压缩包\n\n## Extract Dialog\n\nextract-password-required = 需要密码\nextract-to = 提取到…\nextract-to-title = 提取到文件夹\n\n## Empty Trash Dialog\n\nempty-trash = 清空回收站\nempty-trash-warning = 回收站中的所有内容会被永久删除\n\n## Mount Error Dialog\n\nmount-error = 无法访问驱动器\n\n## New File/Folder Dialog\n\ncreate-new-file = 新建文件\ncreate-new-folder = 新建文件夹\nfile-name = 文件名称\nfolder-name = 文件夹名称\nfile-already-exists = 同名文件已存在\nfolder-already-exists = 同名文件夹已存在\nname-hidden = 以 “.” 开头的文件会被隐藏\nname-invalid = 名称不能是 “{ $filename }”\nname-no-slashes = 名称不可以包含斜线\n\n## Open/Save Dialog\n\ncancel = 取消\ncreate = 创建\nopen = 打开\nopen-file = 打开文件\nopen-folder = 打开文件夹\nopen-in-new-tab = 在新标签中打开\nopen-in-new-window = 在新窗口中打开\nopen-item-location = 打开项目位置\nopen-multiple-files = 打开多个文件\nopen-multiple-folders = 打开多个文件夹\nsave = 保存\nsave-file = 保存文件\n\n## Open With Dialog\n\nopen-with-title = 您想要如何打开 “{ $name }”？\nbrowse-store = 浏览 { $store }\nother-apps = 其他应用程序\nrelated-apps = 相关应用程序\n\n## Permanently delete Dialog\n\nselected-items = 选中的 { $items } 个项目\npermanently-delete-question = 永久删除？\ndelete = 删除\npermanently-delete-warning = { $target } 将被永久删除。此操作无法撤销。\n\n## Rename Dialog\n\nrename-file = 重命名文件\nrename-folder = 重命名文件夹\n\n## Replace Dialog\n\nreplace = 替换\nreplace-title = “{ $filename }” 已存在于该位置\nreplace-warning = 您想要使用您现在正在保存的文件替换掉它吗？一旦替换将会覆盖其内容。\nreplace-warning-operation = 您想要替换掉它吗？一旦替换将会覆盖其内容。\noriginal-file = 原始文件\nreplace-with = 替换为\napply-to-all = 全部应用\nkeep-both = 保留两者\nskip = 跳过\n\n## Set as Executable and Launch Dialog\n\nset-executable-and-launch = 设置为可执行文件并启动\nset-executable-and-launch-description = 您想要将 “{ $name }” 设置为可执行文件并启动它吗？\nset-and-launch = 设置并启动\n\n## Metadata Dialog\n\nopen-with = 打开方式\nowner = 所有者\ngroup = 用户组\nother = 其他用户\n\n### Mode 0\n\nnone = 无\n\n### Mode 1 (unusual)\n\nexecute-only = 仅执行\n\n### Mode 2 (unusual)\n\nwrite-only = 仅写入\n\n### Mode 3 (unusual)\n\nwrite-execute = 写入和执行\n\n### Mode 4\n\nread-only = 只读\n\n### Mode 5\n\nread-execute = 读取和执行\n\n### Mode 6\n\nread-write = 读取和写入\n\n### Mode 7\n\nread-write-execute = 读取、写入和执行\n\n## Favorite Path Error Dialog\n\nfavorite-path-error = 打开路径时出错\nfavorite-path-error-description =\n    无法打开 \"{ $path }\" 。\n    \"{ $path }\" 可能不存在或您没有权限打开它。\n\n    您想要从侧边栏中移除它吗？\nremove = 移除\nkeep = 保留\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = 添加网络驱动器\nconnect = 连接\nconnect-anonymously = 匿名连接\nconnecting = 正在连接…\ndomain = 网络域\nenter-server-address = 输入服务器地址\nnetwork-drive-description =\n    服务器地址包含协议前缀和地址。\n    示例: ssh://192.168.0.1，ftp://[2001:db8::1]\n\n### Make sure to keep the comma which separates the columns\n\nnetwork-drive-schemes =\n    可用协议，前缀\n    AppleTalk，afp://\n    文件传输协议，ftp:// 或者 ftps://\n    网络文件系统，nfs://\n    服务器消息块，smb://\n    SSH 文件传输协议，sftp:// 或者 ssh://\n    WebDav，dav:// 或者 davs://\nnetwork-drive-error = 无法访问网络驱动器\npassword = 密码\nremember-password = 记住密码\ntry-again = 重试\nusername = 用户名\n\n## Operations\n\ncancelled = 已取消\nedit-history = 编辑历史记录\nhistory = 历史记录\nno-history = 历史记录为空。\npending = 待处理\nprogress = { $percent }%\nprogress-cancelled = { $percent }%，已取消\nprogress-paused = { $percent }%，已暂停\nfailed = 失败\ncomplete = 完成\ncompressing =\n    正在从“{ $from }”压缩 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”（{ $progress }）…\ncompressed =\n    已从“{ $from }”压缩 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”\ncopy_noun = 复制\ncreating = 正在“{ $parent }”里创建“{ $name }”\ncreated = 已在“{ $parent }”里创建“{ $name }”\ncopying =\n    正在从“{ $from }”复制 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”（{ $progress }）…\ncopied =\n    已从“{ $from }”复制 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”\ndeleting =\n    正在从{ trash }删除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }（{ $progress }）…\ndeleted =\n    已从{ trash }删除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nemptying-trash = 正在清空{ trash }（{ $progress }）…\nemptied-trash = 已清空{ trash }\nextracting =\n    正在从“{ $from }”提取{ $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”（{ $progress }）…\nextracted =\n    已从“{ $from }”提取 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”\nsetting-executable-and-launching = 设置 \"{ $name }\" 为可执行文件并启动\nset-executable-and-launched = 已设 \"{ $name }\" 为可执行文件并且启动\nsetting-permissions = 正在设置 \"{ $name }\" 的权限为 { $mode }\nset-permissions = 设置 \"{ $name }\" 的权限为 { $mode }\nmoving =\n    正在从“{ $from }”移动 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”（{ $progress }）…\nmoved =\n    已从“{ $from }”移动 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }到“{ $to }”\npermanently-deleting =\n    正在永久删除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\npermanently-deleted =\n    已永久删除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nremoving-from-recents =\n    正在从{ recents }中移除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nremoved-from-recents =\n    已从{ recents }中移除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nrenaming = 正在将“{ $from }”重命名为“{ $to }”\nrenamed = 已重命名\"{ $from }\"为\"{ $to }\"\nrestoring =\n    正在从{ trash }中还原 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }（{ $progress }）…\nrestored =\n    已从{ trash }中还原 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nunknown-folder = 未知文件夹\n\n## Open with\n\nmenu-open-with = 打开方式…\ndefault-app = { $name }（默认）\n\n## Show details\n\nshow-details = 显示详情\ntype = 文件类型：{ $mime }\nitems = 文件数：{ $items }\nitem-size = 文件大小：{ $size }\nitem-created = 创建于：{ $created }\nitem-modified = 修改于：{ $modified }\nitem-accessed = 访问于：{ $accessed }\ncalculating = 计算中…\n\n## Settings\n\nsettings = 设置\nsingle-click = 单击打开\n\n### Appearance\n\nappearance = 外观\ntheme = 主题\nmatch-desktop = 匹配桌面\ndark = 暗色模式\nlight = 亮色模式\n\n### Type to Search\n\ntype-to-search = 输入即可搜索\ntype-to-search-recursive = 搜索当前文件夹及其所有子文件夹\ntype-to-search-enter-path = 输入文件夹或文件路径\n# Context menu\nadd-to-sidebar = 加入侧边栏\ncompress = 压缩…\ndelete-permanently = 永久删除\neject = 弹出\nextract-here = 解压到此处\nnew-file = 新建文件…\nnew-folder = 新建文件夹…\nopen-in-terminal = 在终端模拟器中打开\nmove-to-trash = 移动到回收站\nrestore-from-trash = 从回收站中还原\nremove-from-sidebar = 从侧边栏中移除\nsort-by-name = 按名称排序\nsort-by-modified = 按修改时间排序\nsort-by-size = 按文件大小排序\nsort-by-trashed = 按删除时间排序\nremove-from-recents = 从最近访问中移除\n\n## Desktop\n\nchange-wallpaper = 更改壁纸…\ndesktop-appearance = 桌面外观…\ndisplay-settings = 显示设置…\n\n# Menu\n\n\n## File\n\nfile = 文件\nnew-tab = 新建标签\nnew-window = 新建窗口\nreload-folder = 刷新文件夹\nrename = 重命名…\nclose-tab = 关闭标签\nquit = 退出\n\n## Edit\n\nedit = 编辑\ncut = 剪切\ncopy = 复制\npaste = 粘贴\nselect-all = 全选\n\n## View\n\nzoom-in = 增大\ndefault-size = 默认大小\nzoom-out = 缩小\nview = 视图\ngrid-view = 表格视图\nlist-view = 列表视图\nshow-hidden-files = 显示隐藏文件\nlist-directories-first = 优先列出目录\ngallery-preview = 图库预览\nmenu-settings = 设置…\nmenu-about = 关于 COSMIC 文件…\n\n## Sort\n\nsort = 排序\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = 最新优先\nsort-oldest-first = 最旧优先\nsort-smallest-to-largest = 从小到大\nsort-largest-to-smallest = 从大到小\nrepository = 仓库\nsupport = 支持\nprogress-failed = { $percent }%，失败\nempty-trash-title = 确定要清空回收站？\ntype-to-search-select = 选择第一个匹配的文件或文件夹\npasted-video = 已粘贴视频\npasted-image = 已粘贴图片\npasted-text = 已粘贴文本\ncopy-to-title = 选择复制目的地\ncopy-to-button-label = 复制\nmove-to-title = 选择移动目的地\nmove-to-button-label = 移动\ncopy-to = 复制至…\nmove-to = 移动至…\ncomment = COSMIC 桌面的文件管理器\nkeywords = 文件夹;管理器;\nclear-recents-history = 清除最近访问历史\ncopy-path = 复制文件路径\nshow-recents = 侧边栏中的最近访问\nmixed = 混合\n"
  },
  {
    "path": "i18n/zh-TW/cosmic_files.ftl",
    "content": "cosmic-files = COSMIC 檔案\nempty-folder = 空資料夾\nempty-folder-hidden = 空資料夾（包含隱藏項目）\nno-results = 找不到結果\nfilesystem = 檔案系統\nhome = 家目錄\nnetworks = 網路\nnotification-in-progress = 檔案操作正在進行中\ntrash = 垃圾桶\nrecents = 最近使用\nundo = 復原\ntoday = 今天\n# List view\nname = 名稱\nmodified = 修改日期\nsize = 大小\n\n# Dialogs\n\n\n## Compress Dialog\n\ncreate-archive = 建立壓縮檔案\n\n## Empty Trash Dialog\n\nempty-trash = 清空垃圾桶\nempty-trash-warning = 垃圾桶中的項目將被永久刪除\n\n## New File/Folder Dialog\n\ncreate-new-file = 建立新檔案\ncreate-new-folder = 建立新資料夾\nfile-name = 檔案名稱\nfolder-name = 資料夾名稱\nfile-already-exists = 相同名稱的檔案已經存在\nfolder-already-exists = 相同名稱的資料夾已經存在\nname-hidden = 以「.」開頭的名稱將會被隱藏\nname-invalid = 名稱不能是「{ $filename }」\nname-no-slashes = 名稱不能包含斜線\n\n## Open/Save Dialog\n\ncancel = 取消\ncreate = 創作\nopen = 開啟\nopen-file = 開啟檔案\nopen-folder = 開啟資料夾\nopen-in-new-tab = 在新分頁中開啟\nopen-in-new-window = 在新視窗中開啟\nopen-item-location = 開啟項目位置\nopen-multiple-files = 開啟多個檔案\nopen-multiple-folders = 開啟多個資料夾\nsave = 儲存\nsave-file = 儲存檔案\n\n## Rename Dialog\n\nrename-file = 重新命名檔案\nrename-folder = 重新命名資料夾\n\n## Replace Dialog\n\nreplace = 取代\nreplace-title = 「{ $filename }」已存在於此位置\nreplace-warning = 你要取代它嗎？取代將覆蓋其內容。\nreplace-warning-operation = 你要取代它嗎？取代將覆蓋其內容。\noriginal-file = 原始檔案\nreplace-with = 取代為\napply-to-all = 套用至全部\nkeep-both = 保留兩者\nskip = 跳過\n\n## Metadata Dialog\n\nowner = 擁有者\ngroup = 群組\nother = 其他\n\n# Context Pages\n\n\n## About\n\n\n## Add Network Drive\n\nadd-network-drive = 新增網路磁碟機\nconnect = 連線\nconnect-anonymously = 匿名連線\nconnecting = 連線中...\ndomain = 網域\nenter-server-address = 輸入伺服器地址\nnetwork-drive-description =\n    伺服器地址包括協定前綴和地址。\n    範例：ssh://192.168.0.1, ftp://[2001:db8::1]\nnetwork-drive-schemes =\n    可用協定，前綴\n    AppleTalk，afp://\n    檔案傳輸協定，ftp:// 或 ftps://\n    網路檔案系統，nfs://\n    伺服器訊息區塊，smb://\n    SSH 檔案傳輸協定，sftp:// 或 ssh://\n    WebDav，dav:// 或 davs://\nnetwork-drive-error = 無法存取網路磁碟機\npassword = 密碼\nremember-password = 記住密碼\ntry-again = 再試一次\nusername = 使用者名稱\n\n## Operations\n\nedit-history = 編輯歷史\nhistory = 歷史紀錄\nno-history = 無歷史記錄項目。\npending = 待處理\nfailed = 失敗\ncomplete = 完成\ncompressing =\n    正在壓縮 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    } 從「{ $from }」到 「{ $to }」（{ $progress }）...\ncompressed =\n    已壓縮 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」到「{ $to }」\ncopy_noun = 複製\ncreating = 正在建立「{ $name }」於「{ $parent }」\ncreated = 已建立「{ $name }」於「{ $parent }」\ncopying =\n    正在複製 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」到「{ $to }」（{ $progress }）...\ncopied =\n    已複製 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」到「{ $to }」\nemptying-trash = 正在清空 { trash }（{ $progress }）…\nemptied-trash = 已經清空 { trash }\nextracting =\n    正在解壓縮 { $items } 項目 { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」至「{ $to }」（{ $progress }）...\nextracted =\n    已解壓縮 { $items } 項目 { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」到「{ $to }」\nmoving =\n    正在移動 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    }從「{ $from }」到「{ $to }」（{ $progress }）...\nmoved =\n    已經移動 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    } 從「{ $from }」至「{ $to }」\nrenaming = 正在重新命名「{ $from }」至「{ $to }」\nrenamed = 已經重新命名「{ $from }」至「{ $to }」\nrestoring =\n    正在還原 { $items } 項目 { $items ->\n        [one] 項目\n       *[other] 項目\n    }自 { trash } （{ $progress }）...\nrestored =\n    已經還原 { $items } 項目 { $items ->\n        [one] 項目\n       *[other] 項目\n    }從 { trash }\nunknown-folder = 不明資料夾\n\n## Open with\n\nmenu-open-with = 開啟檔案...\ndefault-app = { $name } （預設）\n\n## Show details\n\nshow-details = 顯示詳細資料\n\n## Settings\n\nsettings = 設定\n\n### Appearance\n\nappearance = 外觀\ntheme = 主題\nmatch-desktop = 符合桌面\ndark = 深色\nlight = 淺色\n# Context menu\nadd-to-sidebar = 添加至側邊欄\ncompress = 壓縮…\nextract-here = 解壓縮\nnew-file = 新建檔案...\nnew-folder = 新建資料夾...\nopen-in-terminal = 在終端機中開啟\nmove-to-trash = 移動至垃圾桶\nrestore-from-trash = 從垃圾桶還原\nremove-from-sidebar = 從側邊欄移除\nsort-by-name = 依名稱排序\nsort-by-modified = 依修改日期排序\nsort-by-size = 依大小排序\n\n# Menu\n\n\n## File\n\nfile = 檔案\nnew-tab = 新建分頁\nnew-window = 新建視窗\nrename = 重新命名...\nclose-tab = 關閉分頁\nquit = 退出\n\n## Edit\n\nedit = 編輯\ncut = 剪下\ncopy = 複製\npaste = 貼上\nselect-all = 全選\n\n## View\n\nzoom-in = 放大\ndefault-size = 預設大小\nzoom-out = 縮小\nview = 檢視\ngrid-view = 網格檢視\nlist-view = 列表檢視\nshow-hidden-files = 顯示隱藏檔案\nlist-directories-first = 目錄優先列出\nmenu-settings = 設定...\nmenu-about = 關於 COSMIC 檔案...\n\n## Sort\n\nsort = 排序\nsort-a-z = A-Z\nsort-z-a = Z-A\nsort-newest-first = 最新優先\nsort-oldest-first = 最舊優先\nsort-smallest-to-largest = 從小到大\nsort-largest-to-smallest = 從大到小\ndeleted =\n    已經刪除 { $items } { $items ->\n        [one] 項目\n       *[other] 項目\n    }從 { trash }\npermanently-deleting =\n    正在永久刪除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\npermanently-deleted =\n    已經永久刪除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nremoving-from-recents =\n    正在從 { recents } 中移除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\ndeleting =\n    正在刪除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }從 { trash }（{ $progress }）…\nremoved-from-recents =\n    已經從 { recents } 中移除 { $items } { $items ->\n        [one] 项目\n       *[other] 项目\n    }\nrepository = 軟體庫源\ndesktop-view-options = 桌面檢視選項...\nshow-on-desktop = 顯示在桌面\ndesktop-folder-content = 桌面資料夾內容\nmounted-drives = 已經掛載的磁碟機\ntrash-folder-icon = 垃圾桶圖示\ntrashed-on = 遺棄時間\nicon-size-and-spacing = 圖示大小與間距\nicon-size = 圖示大小\ngrid-spacing = 網格間距\ndetails = 詳情\ndismiss = 撤停訊息\ndelete = 刪除\nremove = 移除\nsupport = 支援\ncancelled = 已取消\nkeywords = 資料夾;管理器;\nempty-trash-title = 清空垃圾桶？\npause = 暫停\nresume = 繼續\nextract-password-required = 需要密碼\nextract-to = 解壓縮至...\nextract-to-title = 解壓縮至資料夾\nmount-error = 無法存取磁碟機\nopen-with-title = 您要如何開啟「{ $name }」？\nbrowse-store = 瀏覽 { $store }\nother-apps = 其他應用程式\nrelated-apps = 相關應用程式\npermanently-delete-question = 永久刪除？\nset-executable-and-launch = 設定為可以執行並啟動\nread-only = 唯讀\nread-execute = 讀取和執行\nread-write = 讀取和寫入\nread-write-execute = 讀取、寫入和執行\nfavorite-path-error = 開啟目錄時發生錯誤\nset-executable-and-launch-description = 您是否要將「{ $name }」設為可執行並啟動它？\nset-and-launch = 設定並啟動\nnone = 無\nexecute-only = 僅執行\nwrite-only = 僅寫入\nwrite-execute = 寫入和執行\noperations-running =\n    { $running } { $running ->\n        [one] 個操作\n       *[other] 個操作\n    }正在執行（{ $percent }%）...\noperations-running-finished =\n    { $running } { $running ->\n        [one] 個操作\n       *[other] 個操作\n    }正在執行（{ $percent }%）， { $finished } 個已經完成...\npermanently-delete-warning = 「{ $target }」將被永久刪除。此操作無法復原。\nopen-with = 開啟檔案\nselected-items = 已經選定 { $items } 個項目\ncopy-to-title = 選擇複製目的地\ncopy-to-button-label = 複製\nmove-to-title = 選擇移動目的地\nmove-to-button-label = 移動\nkeep = 保留\nprogress = { $percent }%\nprogress-cancelled = { $percent }%，已經取消\nprogress-failed = { $percent }%，失敗\nprogress-paused = { $percent }%，已經暫停\nfavorite-path-error-description =\n    無法開啟「{ $path }」\n    「{ $path }」可能不存在，或您可能沒有權限開啟它。\n\n    您是否要將它從側邊欄移除？\ncomment = COSMIC 桌面檔案管理器\npasted-image = 已經貼上的圖片\npasted-text = 已經貼上的文字\npasted-video = 已經貼上的影片\nsort-by-trashed = 依丟入時間排序\ncalculating = 計算中...\nsingle-click = 點按以開啟\ntype-to-search = 輸入進行搜尋\ntype-to-search-recursive = 搜尋目前資料夾及全部子資料夾\ntype-to-search-enter-path = 輸入目錄或檔案的路徑\ndelete-permanently = 永久刪除\neject = 彈出\nremove-from-recents = 從最近項目中移除\nchange-wallpaper = 變更桌布...\ndesktop-appearance = 桌面外觀...\ndisplay-settings = 顯示設定...\nreload-folder = 重新載入資料夾\ngallery-preview = 圖庫預覽\ntype = 類型：{ $mime }\nitems = 項目：{ $items }\nitem-size = 大小：{ $size }\nitem-created = 建立時間：{ $created }\nitem-modified = 修改時間：{ $modified }\nitem-accessed = 存取時間：{ $accessed }\ntype-to-search-select = 選取第一個符合條件的檔案或資料夾\ncopy-to = 複製至...\nmove-to = 移動至...\nshow-recents = 側邊欄中的最近使用資料夾\nclear-recents-history = 清除最近使用歷史記錄\ncopy-path = 複製路徑\nsetting-executable-and-launching = 設定「{ $name }」為可以執行並進行啟動\nset-executable-and-launched = 設定「{ $name }」為可以執行並已經啟動\nsetting-permissions = 設定「{ $name }」的權限至 { $mode }\nset-permissions = 設定「{ $name }」的權限至 { $mode }\nmixed = 混合\n"
  },
  {
    "path": "i18n.toml",
    "content": "fallback_language = \"en\"\n\n[fluent]\nassets_dir = \"i18n\""
  },
  {
    "path": "justfile",
    "content": "name := 'cosmic-files'\nexport APPID := 'com.system76.CosmicFiles'\n\nrootdir := ''\nprefix := '/usr'\n\nbase-dir := absolute_path(clean(rootdir / prefix))\n\nexport INSTALL_DIR := base-dir / 'share'\n\ncargo-target-dir := env('CARGO_TARGET_DIR', 'target')\nbin-src := cargo-target-dir / 'release' / name\nbin-dst := base-dir / 'bin' / name\n\napplet-name := name + '-applet'\napplet-src := cargo-target-dir / 'release' / applet-name\napplet-dst := base-dir / 'bin' / applet-name\n\ndesktop := APPID + '.desktop'\ndesktop-src := 'target/xdgen' / desktop\ndesktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop\n\nmetainfo := APPID + '.metainfo.xml'\nmetainfo-src := 'target/xdgen' / metainfo\nmetainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo\n\nicons-src := 'res' / 'icons' / 'hicolor'\nicons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor'\n\n# Default recipe which runs `just build-release`\ndefault: build-release\n\n# Runs `cargo clean`\nclean:\n    cargo clean\n\n# Removes vendored dependencies\nclean-vendor:\n    rm -rf .cargo vendor vendor.tar\n\n# `cargo clean` and removes vendored dependencies\nclean-dist: clean clean-vendor\n\n# Compiles with debug profile\nbuild-debug *args:\n    cargo build {{args}}\n    cargo build --package {{applet-name}} {{args}}\n\n# Compiles with release profile\nbuild-release *args: (build-debug '--release' args)\n \n# Compiles applet with release profile\nbuild-release-applet *args:\n    cargo build --package {{applet-name}} --release {{args}}\n\n# Compiles release profile with vendored dependencies\nbuild-vendored *args: vendor-extract (build-release '--frozen --offline' args)\n\n# Runs a clippy check\ncheck *args:\n    cargo clippy --all-features {{args}} -- -W clippy::pedantic\n\n# Runs a clippy check with JSON message format\ncheck-json: (check '--message-format=json')\n\n# Developer target\ndev *args:\n    cargo fmt\n    just run {{args}}\n\n# Run with debug logs\nrun *args:\n    cargo build --release\n    env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full {{bin-src}} {{args}}\n\n# Run tests\ntest *args:\n    cargo test {{args}}\n\nflamegraph *args:\n    cargo flamegraph --release --bin cosmic-files -- --no-daemon {{args}}\n    xdg-open flamegraph.svg\n\nheaptrack *args:\n    #!/usr/bin/env bash\n    set -ex\n    rm -fv heaptrack.cosmic-files.*\n    cargo heaptrack --profile release-with-debug --bin cosmic-files -- --no-daemon {{args}}\n    zstd -dc < heaptrack.cosmic-files.*.raw.zst | /usr/lib/heaptrack/libexec/heaptrack_interpret | zstd -c > heaptrack.cosmic-files.zst\n    heaptrack_gui heaptrack.cosmic-files.zst\n\n# Installs files\ninstall:\n    install -Dm0755 {{bin-src}} {{bin-dst}}\n    install -Dm0755 {{applet-src}} {{applet-dst}}\n    install -Dm0644 {{desktop-src}} {{desktop-dst}}\n    install -Dm0644 {{metainfo-src}} {{metainfo-dst}}\n    for size in `ls {{icons-src}}`; do \\\n        install -Dm0644 \"{{icons-src}}/$size/apps/{{APPID}}.svg\" \"{{icons-dst}}/$size/apps/{{APPID}}.svg\"; \\\n    done\n\n# Installs applet files\ninstall-applet:\n    install -Dm0755 {{applet-src}} {{applet-dst}}\n\n# Uninstalls installed files\nuninstall:\n    rm -f {{bin-dst}} {{applet-dst}}\n\n# Vendor dependencies locally\nvendor:\n    #!/usr/bin/env bash\n    mkdir -p .cargo\n    cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml\n    echo 'directory = \"vendor\"' >> .cargo/config.toml\n    echo >> .cargo/config.toml\n    echo '[env]' >> .cargo/config.toml\n    if [ -n \"${SOURCE_DATE_EPOCH}\" ]\n    then\n        source_date=\"$(date -d \"@${SOURCE_DATE_EPOCH}\" \"+%Y-%m-%d\")\"\n        echo \"VERGEN_GIT_COMMIT_DATE = \\\"${source_date}\\\"\" >> .cargo/config.toml\n    fi\n    if [ -n \"${SOURCE_GIT_HASH}\" ]\n    then\n        echo \"VERGEN_GIT_SHA = \\\"${SOURCE_GIT_HASH}\\\"\" >> .cargo/config.toml\n    fi\n    tar pcf vendor.tar .cargo vendor\n    rm -rf .cargo vendor\n\n# Extracts vendored dependencies\nvendor-extract:\n    rm -rf vendor\n    tar pxf vendor.tar\n"
  },
  {
    "path": "res/com.system76.CosmicFiles.desktop",
    "content": "[Desktop Entry]\nName=COSMIC Files\nComment=File manager for the COSMIC desktop\nExec=cosmic-files %U\nTerminal=false\nType=Application\nStartupNotify=true\nIcon=com.system76.CosmicFiles\nCategories=COSMIC;Utility;FileManager;\nKeywords=Folder;Manager;\nMimeType=inode/directory;\n"
  },
  {
    "path": "res/com.system76.CosmicFiles.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>com.system76.CosmicFiles</id>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>GPL-3.0-only</project_license>\n  <project_group>COSMIC</project_group>\n  <developer_name>System76</developer_name>\n  <update_contact>jeremy@system76.com</update_contact>\n  <url type=\"homepage\">https://github.com/pop-os/cosmic-files</url>\n  <url type=\"bugtracker\">https://github.com/pop-os/cosmic-files</url>\n  <name>COSMIC Files</name>\n  <summary>File manager for the COSMIC desktop</summary>\n  <description>\n    <p>File manager for the COSMIC desktop</p>\n    <p xml:lang=\"ar\">مدير ملفات لسطح مكتب COSMIC</p>\n    <p xml:lang=\"cs\">Správce souborů pro prostředí COSMIC</p>\n    <p xml:lang=\"pl\">Menedżer plików pulpitu COSMIC</p>\n    <p xml:lang=\"hu\">Fájlkezelő a COSMIC asztali környezethez</p>\n    <p xml:lang=\"pt_BR\">Gerenciador de arquivos do COSMIC</p>\n    <p xml:lang=\"pt\">Gerenciador de arquivos do COSMIC</p>\n    <p xml:lang=\"sk\">Správca súborov pre prostredie COSMIC</p>\n    <p xml:lang=\"es\">Gestor de archivos de COSMIC</p>\n    <p xml:lang=\"it\">File manager di COSMIC</p>\n    <p xml:lang=\"sv\">Filhanterare för skrivbordsmiljön COSMIC</p>\n  </description>\n  <keywords>\n    <keyword>Folder</keyword>\n    <keyword>Manager</keyword>\n  </keywords>\n  <launchable type=\"desktop-id\">com.system76.CosmicFiles.desktop</launchable>\n  <icon type=\"remote\" height=\"256\" width=\"256\">https://raw.githubusercontent.com/pop-os/cosmic-files/master/res/icons/hicolor/256x256/apps/com.system76.CosmicFiles.svg</icon>\n  <provides>\n    <id>com.system76.CosmicApplication</id>\n    <mimetypes>\n      <mimetype>inode/directory</mimetype>\n    </mimetypes>\n    <binaries>\n      <binary>cosmic-files</binary>\n    </binaries>\n  </provides>\n</component>\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.93.0\"\ncomponents = [\"clippy\", \"rustfmt\"]\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "imports_granularity = \"Module\"\n"
  },
  {
    "path": "samples/i18n/منزل",
    "content": ""
  },
  {
    "path": "samples/i18n/主目錄",
    "content": ""
  },
  {
    "path": "samples/mime/application/javascript.js",
    "content": "console.log(\"This is an example JavaScript source file\");\n"
  },
  {
    "path": "samples/mime/application/x-shellscript.sh",
    "content": "#!/bin/sh\n\necho \"This is an example shell file\"\n"
  },
  {
    "path": "samples/mime/application/x-yaml.yaml",
    "content": "key: value\n"
  },
  {
    "path": "samples/mime/check.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")\"\n\nfor file in */*\ndo\n    filetype=\"$(xdg-mime query filetype \"${file}\")\"\n    if [ -z \"${filetype}\" ]\n    then\n        echo \"${file}: no filetype found\"\n        exit 1\n    fi\n    if [ \"${file%.*}\" != \"${filetype}\" ]\n    then\n        echo \"${file} is not named according to filetype ${filetype}\"\n        exit 1\n    fi\n\n    default=\"$(xdg-mime query default \"${filetype}\")\"\n    if [ -n \"${default}\" ]\n    then\n        echo \"${file}: ${filetype}: ${default}\"\n    else\n        echo \"${file}: ${filetype}: no default application found\"\n    fi\ndone\n"
  },
  {
    "path": "samples/mime/text/css.css",
    "content": "example-css-file {\n    color: #000000;\n}\n"
  },
  {
    "path": "samples/mime/text/csv.csv",
    "content": "Example,CSV,file\n"
  },
  {
    "path": "samples/mime/text/html.html",
    "content": "<body>\n    Example HTML file\n</body>\n"
  },
  {
    "path": "samples/mime/text/markdown.md",
    "content": "# Markdown\n\nThis is an example markdown file\n"
  },
  {
    "path": "samples/mime/text/plain.txt",
    "content": "This is a plain text file.\n"
  },
  {
    "path": "samples/mime/text/rust.rs",
    "content": "fn main() {\n    println!(\"This is an example Rust source file\");\n}\n"
  },
  {
    "path": "samples/mime/text/x-chdr.h",
    "content": "// This is an example C header file\n"
  },
  {
    "path": "samples/mime/text/x-csrc.c",
    "content": "#include <stdio.h>\n\nint main(int argc, char **argv) {\n    printf(\"This is an example C source file\\n\");\n}\n"
  },
  {
    "path": "samples/mode/.gitignore",
    "content": "/0*\n"
  },
  {
    "path": "samples/mode/create.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\nrm -f 0*\nfor umode in 4 5 6 7\ndo\n    for gmode in 0 4 5 6 7\n    do\n        for amode in 0 4 5 6 7\n        do\n            mode=\"0${umode}${gmode}${amode}\"\n            touch \"${mode}\"\n            chmod \"${mode}\" \"${mode}\"\n        done\n    done\ndone\n\n"
  },
  {
    "path": "scripts/copy.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\ncargo fmt\ncargo build --release --example copy\nrm -rf test\nmkdir test\ncp -a samples test/a\nmkdir test/a/link\ntouch test/a/link/a\nln -s a test/a/link/b\nmkdir test/a/perms\ntouch test/a/perms/400\nchmod 400 test/a/perms/400\ntouch test/a/perms/600\nchmod 600 test/a/perms/600\ntouch test/a/perms/700\nchmod 700 test/a/perms/700\ntime target/release/examples/copy\nls -lR test\nmeld test/a test/c\n"
  },
  {
    "path": "src/app.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::app::{self, Core, Task, context_drawer};\nuse cosmic::cosmic_config::{self, ConfigSet};\nuse cosmic::iced::clipboard::dnd::DndAction;\nuse cosmic::iced::core::SmolStr;\nuse cosmic::iced::core::widget::operation::focusable::unfocus;\nuse cosmic::iced::futures::{self, SinkExt};\nuse cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};\n#[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\nuse cosmic::iced::platform_specific::shell::wayland::commands::overlap_notify::overlap_notify;\nuse cosmic::iced::runtime::{clipboard, task};\nuse cosmic::iced::widget::button::focus;\nuse cosmic::iced::widget::scrollable;\nuse cosmic::iced::widget::scrollable::AbsoluteOffset;\nuse cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId};\nuse cosmic::iced::{\n    self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream,\n};\n#[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\nuse cosmic::iced::{\n    Limits, Point,\n    event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent},\n    platform_specific::runtime::wayland::layer_surface::{\n        IcedMargin, IcedOutput, SctkLayerSurfaceSettings,\n    },\n    platform_specific::shell::wayland::commands::layer_surface::{\n        Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface,\n    },\n};\nuse cosmic::widget::about::About;\nuse cosmic::widget::dnd_destination::DragId;\nuse cosmic::widget::menu::action::MenuAction;\nuse cosmic::widget::menu::key_bind::KeyBind;\nuse cosmic::widget::segmented_button::{self, Entity, ReorderEvent};\nuse cosmic::widget::{self, icon, settings, space};\nuse cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme};\nuse mime_guess::Mime;\nuse notify_debouncer_full::notify::{self, RecommendedWatcher};\nuse notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};\nuse rustc_hash::{FxHashMap, FxHashSet};\nuse slotmap::Key as SlotMapKey;\nuse std::any::TypeId;\nuse std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};\nuse std::future::Future;\nuse std::num::NonZeroU16;\nuse std::path::{Path, PathBuf};\nuse std::pin::Pin;\nuse std::sync::{Arc, LazyLock, Mutex};\nuse std::time::{self, Duration, Instant};\nuse std::{env, fmt, fs, io, process};\nuse tokio::sync::mpsc;\nuse trash::TrashItem;\n#[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\nuse wayland_client::{Proxy, protocol::wl_output::WlOutput};\n\nuse crate::clipboard::{\n    ClipboardCache, ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage,\n    ClipboardPasteText, ClipboardPasteVideo,\n};\nuse crate::config::{\n    AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,\n    TimeConfig, TypeToSearch,\n};\nuse crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};\nuse crate::key_bind::key_binds;\nuse crate::localize::LANGUAGE_SORTER;\nuse crate::mime_app::{self, MimeApp, MimeAppCache};\nuse crate::mounter::{\n    MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage,\n};\nuse crate::operation::{\n    Controller, Operation, OperationError, OperationErrorType, OperationSelection, ReplaceResult,\n    copy_unique_path,\n};\nuse crate::spawn_detached::spawn_detached;\nuse crate::tab::{\n    self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,\n    SearchLocation, Tab,\n};\nuse crate::trash::{Trash, TrashExt};\nuse crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};\nuse crate::{FxOrderMap, context_action, fl, home_dir, menu, mime_icon};\n\nstatic PERMANENT_DELETE_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"permanent-delete-button\"));\n\nstatic DELETE_TRASH_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"delete-trash-button\"));\n\nstatic CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"confirm-open-with-button\"));\n\nstatic CONFIRM_CONTEXT_ACTION_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"confirm-context-action-button\"));\n\nstatic EMPTY_TRASH_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"empty-trash-button\"));\n\nstatic SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"set-executable-and-launch-confirm-button\"));\n\nstatic FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"favorite-path-error-remove-button\"));\n\nstatic MOUNT_ERROR_TRY_AGAIN_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"mount-error-try-again-button\"));\n\npub(crate) static REPLACE_BUTTON_ID: LazyLock<widget::Id> =\n    LazyLock::new(|| widget::Id::new(\"replace-button\"));\n\n#[derive(Clone, Debug)]\npub enum Mode {\n    App,\n    Desktop,\n}\n\n#[derive(Clone, Debug)]\npub struct Flags {\n    pub config_handler: Option<cosmic_config::Config>,\n    pub config: Config,\n    pub state_handler: Option<cosmic_config::Config>,\n    pub state: State,\n    pub mode: Mode,\n    pub locations: Vec<Location>,\n    pub uris: Vec<url::Url>,\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub enum Action {\n    About,\n    AddToSidebar,\n    Compress,\n    Copy,\n    CopyPath,\n    CopyTo,\n    Cut,\n    CosmicSettingsDesktop,\n    CosmicSettingsDisplays,\n    CosmicSettingsWallpaper,\n    DesktopViewOptions,\n    Delete,\n    EditHistory,\n    EditLocation,\n    Eject,\n    EmptyTrash,\n    #[cfg(feature = \"desktop\")]\n    ExecEntryAction(usize),\n    ExtractHere,\n    ExtractTo,\n    Gallery,\n    HistoryNext,\n    HistoryPrevious,\n    ItemDown,\n    ItemLeft,\n    ItemRight,\n    ItemUp,\n    LocationUp,\n    MoveTo,\n    NewFile,\n    NewFolder,\n    Open,\n    OpenInNewTab,\n    OpenInNewWindow,\n    OpenItemLocation,\n    OpenTerminal,\n    OpenWith,\n    RunContextAction(usize),\n    Paste,\n    PermanentlyDelete,\n    Preview,\n    Reload,\n    RemoveFromRecents,\n    Rename,\n    RestoreFromTrash,\n    SearchActivate,\n    SelectFirst,\n    SelectLast,\n    SelectAll,\n    SetSort(HeadingOptions, bool),\n    Settings,\n    TabClose,\n    TabNew,\n    TabNext,\n    TabPrev,\n    TabViewGrid,\n    TabViewList,\n    ToggleFoldersFirst,\n    ToggleShowHidden,\n    ToggleSort(HeadingOptions),\n    WindowClose,\n    WindowNew,\n    ZoomDefault,\n    ZoomIn,\n    ZoomOut,\n    Recents,\n}\n\nimpl Action {\n    const fn message(&self, entity_opt: Option<Entity>) -> Message {\n        match self {\n            Self::About => Message::ToggleContextPage(ContextPage::About),\n            Self::AddToSidebar => Message::AddToSidebar(entity_opt),\n            Self::Compress => Message::Compress(entity_opt),\n            Self::Copy => Message::Copy(entity_opt),\n            Self::CopyPath => Message::CopyPath(entity_opt),\n            Self::CopyTo => Message::CopyTo(entity_opt),\n            Self::Cut => Message::Cut(entity_opt),\n            Self::CosmicSettingsDesktop => Message::CosmicSettings(\"desktop\"),\n            Self::CosmicSettingsDisplays => Message::CosmicSettings(\"displays\"),\n            Self::CosmicSettingsWallpaper => Message::CosmicSettings(\"wallpaper\"),\n            Self::Delete => Message::Delete(entity_opt),\n            Self::DesktopViewOptions => Message::DesktopViewOptions,\n            Self::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),\n            Self::EditLocation => Message::TabMessage(entity_opt, tab::Message::EditLocationEnable),\n            Self::Eject => Message::Eject,\n            Self::EmptyTrash => Message::TabMessage(None, tab::Message::EmptyTrash),\n            Self::ExtractHere => Message::ExtractHere(entity_opt),\n            Self::ExtractTo => Message::ExtractTo(entity_opt),\n            #[cfg(feature = \"desktop\")]\n            Self::ExecEntryAction(action) => {\n                Message::TabMessage(entity_opt, tab::Message::ExecEntryAction(None, *action))\n            }\n            Self::Gallery => Message::TabMessage(entity_opt, tab::Message::GalleryToggle),\n            Self::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext),\n            Self::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),\n            Self::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),\n            Self::ItemLeft => Message::TabMessage(entity_opt, tab::Message::ItemLeft),\n            Self::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight),\n            Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp),\n            Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp),\n            Self::MoveTo => Message::MoveTo(entity_opt),\n            Self::NewFile => Message::NewItem(entity_opt, false),\n            Self::NewFolder => Message::NewItem(entity_opt, true),\n            Self::Open => Message::TabMessage(entity_opt, tab::Message::Open(None)),\n            Self::OpenInNewTab => Message::OpenInNewTab(entity_opt),\n            Self::OpenInNewWindow => Message::OpenInNewWindow(entity_opt),\n            Self::OpenItemLocation => Message::OpenItemLocation(entity_opt),\n            Self::OpenTerminal => Message::OpenTerminal(entity_opt),\n            Self::OpenWith => Message::OpenWithDialog(entity_opt),\n            Self::RunContextAction(action) => {\n                Message::TabMessage(entity_opt, tab::Message::RunContextAction(*action))\n            }\n            Self::Paste => Message::Paste(entity_opt),\n            Self::PermanentlyDelete => Message::PermanentlyDelete(entity_opt),\n            Self::Preview => Message::Preview(entity_opt),\n            Self::Reload => Message::TabMessage(entity_opt, tab::Message::Reload),\n            Self::RemoveFromRecents => Message::RemoveFromRecents(entity_opt),\n            Self::Rename => Message::Rename(entity_opt),\n            Self::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),\n            Self::SearchActivate => Message::SearchActivate,\n            Self::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),\n            Self::SelectFirst => Message::TabMessage(entity_opt, tab::Message::SelectFirst),\n            Self::SelectLast => Message::TabMessage(entity_opt, tab::Message::SelectLast),\n            Self::SetSort(sort, dir) => {\n                Message::TabMessage(entity_opt, tab::Message::SetSort(*sort, *dir))\n            }\n            Self::Settings => Message::ToggleContextPage(ContextPage::Settings),\n            Self::TabClose => Message::TabClose(entity_opt),\n            Self::TabNew => Message::TabNew,\n            Self::TabNext => Message::TabNext,\n            Self::TabPrev => Message::TabPrev,\n            Self::TabViewGrid => Message::TabView(entity_opt, tab::View::Grid),\n            Self::TabViewList => Message::TabView(entity_opt, tab::View::List),\n            Self::ToggleFoldersFirst => Message::ToggleFoldersFirst,\n            Self::ToggleShowHidden => Message::ToggleShowHidden,\n            Self::ToggleSort(sort) => {\n                Message::TabMessage(entity_opt, tab::Message::ToggleSort(*sort))\n            }\n            Self::WindowClose => Message::WindowClose,\n            Self::WindowNew => Message::WindowNew,\n            Self::ZoomDefault => Message::ZoomDefault(entity_opt),\n            Self::ZoomIn => Message::ZoomIn(entity_opt),\n            Self::ZoomOut => Message::ZoomOut(entity_opt),\n            Self::Recents => Message::Recents,\n        }\n    }\n}\n\nimpl MenuAction for Action {\n    type Message = Message;\n\n    fn message(&self) -> Message {\n        self.message(None)\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct PreviewItem(pub Box<tab::Item>);\n\nimpl PartialEq for PreviewItem {\n    fn eq(&self, other: &Self) -> bool {\n        self.0.location_opt == other.0.location_opt\n    }\n}\n\nimpl Eq for PreviewItem {}\n\n#[derive(Clone, Debug, Eq, PartialEq)]\npub enum PreviewKind {\n    Custom(PreviewItem),\n    Location(Location),\n    Selected,\n}\n\n#[derive(Copy, Clone, Debug, Eq, PartialEq)]\npub enum NavMenuAction {\n    ClearRecents,\n    EmptyTrash,\n    Open(segmented_button::Entity),\n    OpenWith(segmented_button::Entity),\n    OpenInNewTab(segmented_button::Entity),\n    OpenInNewWindow(segmented_button::Entity),\n    Preview(segmented_button::Entity),\n    RunContextAction(segmented_button::Entity, usize),\n    RemoveFromSidebar(segmented_button::Entity),\n}\n\nimpl MenuAction for NavMenuAction {\n    type Message = cosmic::Action<Message>;\n\n    fn message(&self) -> Self::Message {\n        cosmic::Action::App(Message::NavMenuAction(*self))\n    }\n}\n\n/// Messages that are used specifically by our [`App`].\n#[derive(Clone, Debug)]\npub enum Message {\n    AddToSidebar(Option<Entity>),\n    AppTheme(AppTheme),\n    CloseToast(widget::ToastId),\n    Compress(Option<Entity>),\n    Config(Config),\n    Copy(Option<Entity>),\n    CopyPath(Option<Entity>),\n    CopyTo(Option<Entity>),\n    CopyToResult(DialogResult),\n    CosmicSettings(&'static str),\n    Cut(Option<Entity>),\n    Delete(Option<Entity>),\n    DesktopConfig(DesktopConfig),\n    DesktopViewOptions,\n    DesktopDialogs(bool),\n    DialogCancel,\n    DialogComplete,\n    Eject,\n    FileDialogMessage(DialogMessage),\n    DialogPush(DialogPage, Option<widget::Id>),\n    DialogUpdate(DialogPage),\n    DialogUpdateComplete(DialogPage),\n    ExtractHere(Option<Entity>),\n    ExtractTo(Option<Entity>),\n    ExtractToResult(DialogResult),\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    Focused(window::Id),\n    Key(window::Id, Modifiers, Key, Option<SmolStr>),\n    LaunchUrl(String),\n    MaybeExit,\n    ModifiersChanged(window::Id, Modifiers),\n    MounterItems(MounterKey, MounterItems),\n    MountResult(MounterKey, MounterItem, Result<bool, String>),\n    Mouse(window::Id, mouse::Button),\n    MoveTo(Option<Entity>),\n    MoveToResult(DialogResult),\n    NavBarClose(Entity),\n    NavBarContext(Entity),\n    NavMenuAction(NavMenuAction),\n    NetworkAuth(MounterKey, String, MounterAuth, mpsc::Sender<MounterAuth>),\n    NetworkDriveInput(String),\n    NetworkDriveOpenEntityAfterMount {\n        entity: Entity,\n    },\n    NetworkDriveOpenTabAfterMount {\n        location: Location,\n    },\n    NetworkDriveSubmit,\n    NetworkResult(MounterKey, String, Result<bool, String>),\n    NewItem(Option<Entity>, bool),\n    #[cfg(feature = \"notify\")]\n    Notification(Arc<Mutex<notify_rust::NotificationHandle>>),\n    NotifyEvents(Vec<DebouncedEvent>),\n    NotifyWatcher(WatcherWrapper),\n    OpenTerminal(Option<Entity>),\n    OpenInNewTab(Option<Entity>),\n    OpenInNewWindow(Option<Entity>),\n    OpenItemLocation(Option<Entity>),\n    OpenWithBrowse,\n    OpenWithDialog(Option<Entity>),\n    OpenWithSelection(usize),\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    Overlap(window::Id, OverlapNotifyEvent),\n    Paste(Option<Entity>),\n    PasteContents(PathBuf, ClipboardPaste),\n    PasteImage(PathBuf),\n    PasteImageContents(PathBuf, ClipboardPasteImage),\n    PasteText(PathBuf),\n    PasteTextContents(PathBuf, ClipboardPasteText),\n    PasteVideo(PathBuf),\n    PasteVideoContents(PathBuf, ClipboardPasteVideo),\n    CheckClipboard,\n    CheckClipboardImage,\n    CheckClipboardVideo,\n    CheckClipboardText,\n    RetryCheckClipboard(ClipboardCache),\n    ClipboardCached(ClipboardCache),\n    PendingCancel(u64),\n    PendingCancelAll,\n    PendingComplete(u64, OperationSelection),\n    PendingDismiss,\n    PendingError(u64, OperationError),\n    PendingResults(Vec<(u64, OperationSelection)>, Vec<(u64, OperationError)>),\n    PendingPause(u64, bool),\n    PendingPauseAll(bool),\n    PermanentlyDelete(Option<Entity>),\n    Preview(Option<Entity>),\n    ReorderTab(ReorderEvent),\n    RescanRecents,\n    RescanTrash,\n    RemoveFromRecents(Option<Entity>),\n    Rename(Option<Entity>),\n    ReplaceResult(ReplaceResult),\n    RestoreFromTrash(Option<Entity>),\n    SaveSortNames,\n    ScrollTab(i16),\n    SearchActivate,\n    SearchClear,\n    SearchInput(String),\n    SetShowDetails(bool),\n    SetShowRecents(bool),\n    SetTypeToSearch(TypeToSearch),\n    SystemThemeModeChange,\n    Size(window::Id, Size),\n    TabActivate(Entity),\n    TabNext,\n    TabPrev,\n    TabClose(Option<Entity>),\n    TabConfig(TabConfig),\n    TabMessage(Option<Entity>, tab::Message),\n    TabNew,\n    TabRescan(\n        Entity,\n        Location,\n        Option<Box<tab::Item>>,\n        Vec<tab::Item>,\n        Option<Vec<PathBuf>>,\n    ),\n    TabView(Option<Entity>, tab::View),\n    TimeConfigChange(TimeConfig),\n    ToggleContextPage(ContextPage),\n    ToggleFoldersFirst,\n    ToggleShowHidden,\n    Undo(usize),\n    UndoTrash(widget::ToastId, Arc<[PathBuf]>),\n    UndoTrashStart(Vec<TrashItem>),\n    WindowClose,\n    WindowCloseRequested(window::Id),\n    WindowMaximize(window::Id, bool),\n    WindowNew,\n    ZoomDefault(Option<Entity>),\n    ZoomIn(Option<Entity>),\n    ZoomOut(Option<Entity>),\n    DndHoverLocTimeout(Location),\n    DndHoverTabTimeout(Entity),\n    DndEnterNav(Entity),\n    DndExitNav,\n    DndEnterTab(Entity, Vec<String>),\n    DndExitTab,\n    DndDropTab(Entity, Option<ClipboardPaste>, DndAction),\n    DndDropNav(Entity, Option<ClipboardPaste>, DndAction),\n    Recents,\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    OutputEvent(OutputEvent, WlOutput),\n    Cosmic(app::Action),\n    None,\n    Surface(surface::Action),\n    CutPaths(Vec<PathBuf>),\n}\n\n#[derive(Clone, Debug, Eq, PartialEq)]\npub enum ContextPage {\n    About,\n    EditHistory,\n    NetworkDrive,\n    Preview(Option<Entity>, PreviewKind),\n    Settings,\n}\n\n#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]\npub enum ArchiveType {\n    Tgz,\n    #[default]\n    Zip,\n}\n\nimpl ArchiveType {\n    pub const fn all() -> &'static [Self] {\n        &[Self::Tgz, Self::Zip]\n    }\n\n    pub const fn extension(&self) -> &str {\n        match self {\n            Self::Tgz => \".tgz\",\n            Self::Zip => \".zip\",\n        }\n    }\n}\n\nimpl AsRef<str> for ArchiveType {\n    fn as_ref(&self) -> &str {\n        self.extension()\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum DialogPage {\n    Compress {\n        paths: Box<[PathBuf]>,\n        to: PathBuf,\n        name: String,\n        archive_type: ArchiveType,\n        password: Option<String>,\n    },\n    EmptyTrash,\n    FailedOperation(u64),\n    FailedOperations(Vec<u64>),\n    ExtractPassword {\n        id: u64,\n        password: String,\n    },\n    MountError {\n        mounter_key: MounterKey,\n        item: MounterItem,\n        error: String,\n    },\n    NetworkAuth {\n        mounter_key: MounterKey,\n        uri: String,\n        auth: MounterAuth,\n        auth_tx: mpsc::Sender<MounterAuth>,\n    },\n    NetworkError {\n        mounter_key: MounterKey,\n        uri: String,\n        error: String,\n    },\n    NewItem {\n        parent: PathBuf,\n        name: String,\n        dir: bool,\n    },\n    RunContextAction {\n        action: usize,\n        paths: Box<[PathBuf]>,\n    },\n    OpenWith {\n        path: PathBuf,\n        mime: mime_guess::Mime,\n        selected: usize,\n        store_opt: Option<MimeApp>,\n    },\n    PermanentlyDelete {\n        paths: Box<[PathBuf]>,\n    },\n    DeleteTrash {\n        items: Vec<TrashItem>,\n    },\n    RenameItem {\n        from: PathBuf,\n        parent: PathBuf,\n        name: String,\n        dir: bool,\n    },\n    Replace {\n        from: Box<tab::Item>,\n        to: Box<tab::Item>,\n        multiple: bool,\n        apply_to_all: bool,\n        conflict_count: usize,\n        tx: mpsc::Sender<ReplaceResult>,\n    },\n    SetExecutableAndLaunch {\n        path: PathBuf,\n    },\n    FavoritePathError {\n        path: PathBuf,\n        entity: Entity,\n    },\n}\n\npub struct DialogPages {\n    pages: VecDeque<DialogPage>,\n}\n\nimpl Default for DialogPages {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl DialogPages {\n    pub const fn new() -> Self {\n        Self {\n            pages: VecDeque::new(),\n        }\n    }\n\n    pub fn front(&self) -> Option<&DialogPage> {\n        self.pages.front()\n    }\n\n    pub fn front_mut(&mut self) -> Option<&mut DialogPage> {\n        self.pages.front_mut()\n    }\n\n    pub fn push_back(&mut self, page: DialogPage) -> Task<Message> {\n        let task = if self.pages.is_empty() {\n            Task::done(cosmic::Action::App(Message::DesktopDialogs(true)))\n        } else {\n            Task::none()\n        };\n        self.pages.push_back(page);\n        task\n    }\n\n    pub fn push_front(&mut self, page: DialogPage) -> Task<Message> {\n        let task = if self.pages.is_empty() {\n            Task::done(cosmic::Action::App(Message::DesktopDialogs(true)))\n        } else {\n            Task::none()\n        };\n        self.pages.push_front(page);\n        task\n    }\n\n    #[must_use]\n    pub fn pop_front(&mut self) -> Option<(DialogPage, Task<Message>)> {\n        let page = self.pages.pop_front()?;\n        let task = if self.pages.is_empty() {\n            Task::done(cosmic::Action::App(Message::DesktopDialogs(false)))\n        } else {\n            Task::none()\n        };\n        Some((page, task))\n    }\n\n    pub fn update_front(&mut self, page: DialogPage) {\n        if !self.pages.is_empty() {\n            self.pages[0] = page;\n        }\n    }\n}\n\npub struct FavoriteIndex(usize);\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\nenum MimeAppMatch {\n    Exact,\n    Related,\n    Other,\n}\n\npub struct MounterData(MounterKey, MounterItem);\n\n#[derive(Clone, Debug)]\npub enum WindowKind {\n    ContextMenu(Entity, widget::Id),\n    Desktop(Entity),\n    DesktopViewOptions,\n    Dialogs(widget::Id),\n    FileDialog(Option<Box<[PathBuf]>>),\n    Preview(Option<Entity>, PreviewKind),\n}\n\npub struct WatcherWrapper {\n    watcher_opt: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,\n}\n\nimpl Clone for WatcherWrapper {\n    fn clone(&self) -> Self {\n        Self { watcher_opt: None }\n    }\n}\n\nimpl fmt::Debug for WatcherWrapper {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"WatcherWrapper\").finish()\n    }\n}\n\nimpl PartialEq for WatcherWrapper {\n    fn eq(&self, _other: &Self) -> bool {\n        false\n    }\n}\n\nstruct Window {\n    kind: WindowKind,\n    modifiers: Modifiers,\n}\n\nimpl Window {\n    fn new(kind: WindowKind) -> Self {\n        Self {\n            kind,\n            modifiers: Modifiers::empty(),\n        }\n    }\n}\n\n// The [`App`] stores application-specific state.\npub struct App {\n    core: Core,\n    about: About,\n    nav_bar_context_id: segmented_button::Entity,\n    nav_model: segmented_button::SingleSelectModel,\n    tab_model: segmented_button::Model<segmented_button::SingleSelect>,\n    config_handler: Option<cosmic_config::Config>,\n    state_handler: Option<cosmic_config::Config>,\n    config: Config,\n    state: State,\n    mode: Mode,\n    app_themes: Vec<String>,\n    compio_tx: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,\n    context_page: ContextPage,\n    dialog_pages: DialogPages,\n    dialog_text_input: widget::Id,\n    key_binds: HashMap<KeyBind, Action>,\n    margin: FxHashMap<window::Id, (f32, f32, f32, f32)>,\n    mime_app_cache: MimeAppCache,\n    modifiers: Modifiers,\n    mounter_items: FxHashMap<MounterKey, MounterItems>,\n    must_save_sort_names: bool,\n    network_drive_connecting: Option<(MounterKey, String)>,\n    network_drive_input: String,\n    #[cfg(feature = \"notify\")]\n    notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    overlap: FxHashMap<String, (window::Id, Rectangle)>,\n    pending_operation_id: u64,\n    pending_operations: BTreeMap<u64, (Operation, Controller)>,\n    progress_operations: BTreeSet<u64>,\n    complete_operations: BTreeMap<u64, Operation>,\n    failed_operations: BTreeMap<u64, (Operation, Controller, String)>,\n    scrollable_id: widget::Id,\n    search_id: widget::Id,\n    size: Option<Size>,\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    layer_sizes: FxHashMap<window::Id, Size>,\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    surface_ids: FxHashMap<WlOutput, WindowId>,\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    surface_names: FxHashMap<WindowId, String>,\n    toasts: widget::toaster::Toasts<Message>,\n    watcher_opt: Option<(\n        Debouncer<RecommendedWatcher, RecommendedCache>,\n        FxHashSet<PathBuf>,\n    )>,\n    windows: FxHashMap<window::Id, Window>,\n    nav_dnd_hover: Option<(Location, Instant)>,\n    tab_dnd_hover: Option<(Entity, Instant)>,\n    type_select_prefix: String,\n    type_select_last_key: Option<Instant>,\n    nav_drag_id: DragId,\n    tab_drag_id: DragId,\n    auto_scroll_speed: Option<i16>,\n    file_dialog_opt: Option<Dialog<Message>>,\n    clipboard_cache: ClipboardCache,\n}\n\nimpl App {\n    /// Returns true if the clipboard cache contains pasteable content\n    fn clipboard_has_content(&self) -> bool {\n        !matches!(self.clipboard_cache, ClipboardCache::Empty)\n    }\n\n    fn push_dialog(&mut self, page: DialogPage, focus_id: Option<widget::Id>) -> Task<Message> {\n        let t = self.dialog_pages.push_back(page);\n        if let Some(focus_id) = focus_id {\n            Task::batch([t, focus(focus_id)])\n        } else {\n            t\n        }\n    }\n\n    fn open_file(&mut self, paths: &[impl AsRef<Path>]) -> Task<Message> {\n        let mut tasks = Vec::new();\n\n        // Associate all paths to its MIME type\n        // This allows handling paths as groups if possible, such as launching a single video\n        // player that is passed every path.\n        let mut groups: FxHashMap<Mime, Vec<PathBuf>> = FxHashMap::default();\n        let mut all_archives = true;\n        let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;\n        for (mime, path) in paths.iter().map(|path| {\n            (\n                mime_icon::mime_for_path(path, None, false),\n                path.as_ref().to_owned(),\n            )\n        }) {\n            if all_archives && !supported_archive_types.iter().copied().any(|t| mime == t) {\n                all_archives = false;\n            }\n            groups.entry(mime).or_default().push(path);\n        }\n\n        if all_archives {\n            // Use extract to dialog if all selected paths are supported archives\n            return self.extract_to(paths);\n        }\n\n        'outer: for (mime, paths) in groups {\n            log::debug!(\"Attempting to launch app\\n\\tfor: {mime}\\n\\twith: {paths:?}\");\n\n            // First launch apps that can be launched directly\n            if mime == \"application/x-desktop\" {\n                #[cfg(feature = \"desktop\")]\n                {\n                    // Try opening desktop application\n                    Self::launch_desktop_entries(&paths);\n                    continue;\n                }\n            } else if mime == \"application/x-executable\" || mime == \"application/vnd.appimage\" {\n                // Try opening executable\n                for path in paths {\n                    let mut command = std::process::Command::new(&path);\n                    match spawn_detached(&mut command) {\n                        Ok(()) => {}\n                        Err(err) => match err.kind() {\n                            io::ErrorKind::PermissionDenied => {\n                                // If permission is denied, try marking as executable, then running\n                                tasks.push(self.push_dialog(\n                                    DialogPage::SetExecutableAndLaunch { path },\n                                    Some(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()),\n                                ));\n                            }\n                            _ => {\n                                log::warn!(\"failed to execute {}: {}\", path.display(), err);\n                            }\n                        },\n                    }\n                }\n                continue;\n            }\n\n            // Try mime apps, which should be faster than xdg-open\n            if self.launch_from_mime_cache(&mime, &paths) {\n                continue;\n            }\n\n            // loop through subclasses if available\n            if let Some(mime_sub_classes) = mime_icon::parent_mime_types(&mime) {\n                for sub_class in mime_sub_classes {\n                    if self.launch_from_mime_cache(&sub_class, &paths) {\n                        continue 'outer;\n                    }\n                }\n            }\n\n            // Fall back to using open crate\n            for path in paths {\n                match open::that_detached(&path) {\n                    Ok(()) => {\n                        if self.config.show_recents {\n                            let _ = recently_used_xbel::update_recently_used(\n                                &path,\n                                Self::APP_ID.to_string(),\n                                \"cosmic-files\".to_string(),\n                                None,\n                            );\n                        }\n                    }\n                    Err(err) => {\n                        log::warn!(\"failed to open {}: {}\", path.display(), err);\n                    }\n                }\n            }\n        }\n\n        Task::batch(tasks)\n    }\n\n    #[cfg(feature = \"desktop\")]\n    fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {\n        use cosmic::desktop::fde::DesktopEntry;\n        let locales = cosmic::desktop::fde::get_languages_from_env();\n\n        for path in paths.iter().map(AsRef::as_ref) {\n            match DesktopEntry::from_path::<&str>(path, None) {\n                Ok(entry) => match entry.exec() {\n                    Some(exec) => {\n                        match mime_app::exec_to_command(\n                            exec,\n                            entry.name(&locales).as_deref().unwrap_or_default(),\n                            Some(path),\n                            &[] as &[&str; 0],\n                        ) {\n                            Some(commands) => {\n                                let cwd_opt = entry.desktop_entry(\"Path\");\n\n                                for mut command in commands {\n                                    if let Some(cwd) = cwd_opt {\n                                        command.current_dir(cwd);\n                                    }\n\n                                    if let Err(err) = spawn_detached(&mut command) {\n                                        log::warn!(\"failed to execute {}: {}\", path.display(), err);\n                                    }\n                                }\n                            }\n                            None => {\n                                log::warn!(\n                                    \"failed to parse {}: invalid Desktop Entry/Exec\",\n                                    path.display()\n                                );\n                            }\n                        }\n                    }\n                    None => {\n                        log::warn!(\n                            \"failed to parse {}: missing Desktop Entry/Exec\",\n                            path.display()\n                        );\n                    }\n                },\n                Err(err) => {\n                    log::warn!(\"failed to parse {}: {}\", path.display(), err);\n                }\n            }\n        }\n    }\n\n    fn launch_from_mime_cache<P>(&self, mime: &Mime, paths: &[P]) -> bool\n    where\n        P: std::fmt::Debug + AsRef<Path> + AsRef<std::ffi::OsStr>,\n    {\n        for app in self.mime_app_cache.get(mime) {\n            let Some(commands) = app.command(paths) else {\n                continue;\n            };\n            let len = commands.len();\n\n            for (i, mut command) in commands.into_iter().enumerate() {\n                match spawn_detached(&mut command) {\n                    Ok(()) => {\n                        if self.config.show_recents {\n                            for path in paths {\n                                let _ = recently_used_xbel::update_recently_used(\n                                    &path.into(),\n                                    Self::APP_ID.to_string(),\n                                    \"cosmic-files\".to_string(),\n                                    None,\n                                );\n                            }\n                        }\n\n                        return true;\n                    }\n                    Err(err) => {\n                        // More than one command: The app doesn't support lists of paths so each command\n                        // is associated with one instance\n                        //\n                        // One command: Attempted to launch one app with multiple paths\n                        let path = if len > 1 {\n                            format!(\"{:?}\", paths.get(i))\n                        } else {\n                            format!(\"{paths:?}\")\n                        };\n                        log::warn!(\"failed to open {:?} with {:?}: {}\", path, app.id, err);\n                    }\n                }\n            }\n        }\n\n        // No app matched for mimes and paths\n        false\n    }\n\n    #[cfg(feature = \"desktop\")]\n    fn exec_entry_action(entry: &cosmic::desktop::DesktopEntryData, action: usize) {\n        if let Some(action) = entry.desktop_actions.get(action) {\n            // Largely copied from COSMIC app library\n            let mut exec = shlex::Shlex::new(&action.exec);\n            match exec.next() {\n                Some(cmd) if !cmd.contains('=') => {\n                    let mut proc = tokio::process::Command::new(cmd);\n                    proc.args(exec.filter(|arg| !arg.starts_with('%')));\n                    let _ = proc.spawn();\n                }\n                _ => (),\n            }\n        } else {\n            log::warn!(\n                \"Invalid actions index `{action}` for desktop entry {}\",\n                entry.name\n            );\n        }\n    }\n\n    fn destination_selection_dialog(\n        &mut self,\n        paths: &[impl AsRef<Path>],\n        on_result: impl Fn(DialogResult) -> Message + 'static,\n        title: impl Into<String>,\n        accept_label: impl AsRef<str>,\n    ) -> Task<Message> {\n        if let Some(destination) = paths\n            .first()\n            .and_then(|first| first.as_ref().parent())\n            .map(Path::to_path_buf)\n        {\n            let (mut dialog, dialog_task) = Dialog::new(\n                DialogSettings::new()\n                    .kind(DialogKind::OpenFolder)\n                    .path(destination),\n                Message::FileDialogMessage,\n                on_result,\n            );\n            let set_title_task = dialog.set_title(title);\n            dialog.set_accept_label(accept_label);\n            self.windows.insert(\n                dialog.window_id(),\n                Window::new(WindowKind::FileDialog(Some(\n                    paths.iter().map(|x| x.as_ref().to_path_buf()).collect(),\n                ))),\n            );\n            self.file_dialog_opt = Some(dialog);\n            Task::batch([set_title_task, dialog_task])\n        } else {\n            Task::none()\n        }\n    }\n\n    fn extract_to(&mut self, paths: &[impl AsRef<Path>]) -> Task<Message> {\n        self.destination_selection_dialog(\n            paths,\n            Message::ExtractToResult,\n            fl!(\"extract-to-title\"),\n            fl!(\"extract-here\"),\n        )\n    }\n\n    fn move_to(&mut self, paths: &[impl AsRef<Path>]) -> Task<Message> {\n        self.destination_selection_dialog(\n            paths,\n            Message::MoveToResult,\n            fl!(\"move-to-title\"),\n            fl!(\"move-to-button-label\"),\n        )\n    }\n\n    fn copy_to(&mut self, paths: &[impl AsRef<Path>]) -> Task<Message> {\n        self.destination_selection_dialog(\n            paths,\n            Message::CopyToResult,\n            fl!(\"copy-to-title\"),\n            fl!(\"copy-to-button-label\"),\n        )\n    }\n\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    fn handle_overlap(&mut self) {\n        let mut overlaps: FxHashMap<_, _> = self\n            .windows\n            .keys()\n            .map(|k| (*k, (0., 0., 0., 0.)))\n            .collect();\n        let mut sorted_overlaps: Box<[_]> = self.overlap.values().collect();\n        sorted_overlaps\n            .sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height)));\n\n        for (w_id, overlap) in sorted_overlaps {\n            let Some((bl, br, tl, tr, mut size)) = self.layer_sizes.get(w_id).map(|s| {\n                (\n                    Rectangle::new(\n                        Point::new(0., s.height / 2.),\n                        Size::new(s.width / 2., s.height / 2.),\n                    ),\n                    Rectangle::new(\n                        Point::new(s.width / 2., s.height / 2.),\n                        Size::new(s.width / 2., s.height / 2.),\n                    ),\n                    Rectangle::new(Point::new(0., 0.), Size::new(s.width / 2., s.height / 2.)),\n                    Rectangle::new(\n                        Point::new(s.width / 2., 0.),\n                        Size::new(s.width / 2., s.height / 2.),\n                    ),\n                    *s,\n                )\n            }) else {\n                continue;\n            };\n            let tl = tl.intersects(overlap);\n            let tr = tr.intersects(overlap);\n            let bl = bl.intersects(overlap);\n            let br = br.intersects(overlap);\n            let Some((top, left, bottom, right)) = overlaps.get_mut(w_id) else {\n                continue;\n            };\n            if tl && tr {\n                *top += overlap.height;\n            }\n            if tl && bl {\n                *left += overlap.width;\n            }\n            if bl && br {\n                *bottom += overlap.height;\n            }\n            if tr && br {\n                *right += overlap.width;\n            }\n\n            let min_dim =\n                if overlap.width / size.width.max(1.) > overlap.height / size.height.max(1.) {\n                    (0., overlap.height)\n                } else {\n                    (overlap.width, 0.)\n                };\n            // just one quadrant with overlap\n            if tl && !(tr || bl) {\n                *top += min_dim.1;\n                *left += min_dim.0;\n\n                size.height -= min_dim.1;\n                size.width -= min_dim.0;\n            }\n            if tr && !(tl || br) {\n                *top += min_dim.1;\n                *right += min_dim.0;\n\n                size.height -= min_dim.1;\n                size.width -= min_dim.0;\n            }\n            if bl && !(br || tl) {\n                *bottom += min_dim.1;\n                *left += min_dim.0;\n\n                size.height -= min_dim.1;\n                size.width -= min_dim.0;\n            }\n            if br && !(bl || tr) {\n                *bottom += min_dim.1;\n                *right += min_dim.0;\n\n                size.height -= min_dim.1;\n                size.width -= min_dim.0;\n            }\n        }\n        self.margin = overlaps;\n    }\n\n    fn open_tab_entity(\n        &mut self,\n        location: Location,\n        activate: bool,\n        selection_paths: Option<Vec<PathBuf>>,\n        scrollable_id: widget::Id,\n        window_id: Option<window::Id>,\n    ) -> (Entity, Task<Message>) {\n        let mut tab = Tab::new(\n            location.clone(),\n            self.config.tab,\n            self.config.thumb_cfg,\n            Some(&self.state.sort_names),\n            scrollable_id,\n            window_id,\n        );\n        tab.mode = match self.mode {\n            Mode::App => tab::Mode::App,\n            Mode::Desktop => {\n                tab.config.view = tab::View::Grid;\n                tab::Mode::Desktop\n            }\n        };\n\n        let entity = self\n            .tab_model\n            .insert()\n            .text(tab.title())\n            .data(tab)\n            .closable();\n\n        let entity = if activate {\n            entity.activate().id()\n        } else {\n            entity.id()\n        };\n\n        let mut tasks = Vec::with_capacity(4);\n        if activate {\n            tasks.push(task::widget(unfocus()));\n        }\n        tasks.push(self.update_title());\n        tasks.push(self.update_watcher());\n        tasks.push(self.update_tab(entity, location, selection_paths));\n        (entity, Task::batch(tasks))\n    }\n\n    fn open_tab(\n        &mut self,\n        location: Location,\n        activate: bool,\n        selection_paths: Option<Vec<PathBuf>>,\n    ) -> Task<Message> {\n        self.open_tab_entity(\n            location,\n            activate,\n            selection_paths,\n            self.scrollable_id.clone(),\n            None,\n        )\n        .1\n    }\n\n    // This wrapper ensures that local folders use trash and remote folders permanently delete with a dialog\n    fn delete(&mut self, paths: impl IntoIterator<Item = PathBuf>) -> Task<Message> {\n        let mut dialog_paths = Vec::new();\n        let mut trash_paths = Vec::new();\n\n        for path in paths {\n            //TODO: is there a smarter way to check this? (like checking for trash folders)\n            let can_trash = match path.metadata() {\n                Ok(metadata) => matches!(tab::fs_kind(&metadata), tab::FsKind::Local),\n                Err(err) => {\n                    log::warn!(\"failed to get metadata for {}: {}\", path.display(), err);\n                    false\n                }\n            };\n            if can_trash {\n                trash_paths.push(path);\n            } else {\n                dialog_paths.push(path);\n            }\n        }\n\n        let mut tasks = Vec::new();\n        if !dialog_paths.is_empty() {\n            tasks.push(self.update(Message::DialogPush(\n                DialogPage::PermanentlyDelete {\n                    paths: dialog_paths.into_boxed_slice(),\n                },\n                Some(PERMANENT_DELETE_BUTTON_ID.clone()),\n            )));\n        }\n        if !trash_paths.is_empty() {\n            tasks.push(self.operation(Operation::Delete { paths: trash_paths }));\n        }\n        Task::batch(tasks)\n    }\n\n    fn operation(&mut self, operation: Operation) -> Task<Message> {\n        let id = self.pending_operation_id;\n        let controller = Controller::default();\n        let compio_tx = self.compio_tx.clone();\n\n        self.pending_operation_id += 1;\n        if operation.show_progress_notification() {\n            self.progress_operations.insert(id);\n        }\n        self.pending_operations\n            .insert(id, (operation.clone(), controller.clone()));\n\n        // Use a task to send operations to the compio runtime thread.\n        cosmic::Task::stream(cosmic::iced::stream::channel(4, move |msg_tx| async move {\n            let (tx, rx) = tokio::sync::oneshot::channel();\n\n            let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));\n\n            let msg_tx_clone = msg_tx.clone();\n\n            _ = compio_tx\n                .send(Box::pin(async move {\n                    let msg = match operation.perform(&msg_tx_clone, controller).await {\n                        Ok(result_paths) => Message::PendingComplete(id, result_paths),\n                        Err(err) => Message::PendingError(id, err),\n                    };\n\n                    _ = tx.send(msg);\n                }))\n                .await;\n\n            if let Ok(msg) = rx.await {\n                let _ = msg_tx.lock().await.send(msg).await;\n            }\n        }))\n        .map(cosmic::Action::App)\n    }\n\n    /// Will join operations together into a single task that will return a single\n    /// Message::PendingResults message when all operations are complete.\n    fn join_operations(&mut self, operations: Vec<Operation>) -> Task<Message> {\n        Task::batch(\n            operations\n                .into_iter()\n                .map(|operation| self.operation(operation)),\n        )\n        .collect()\n        .map(|messages| {\n            let results = messages.into_iter().fold(\n                Message::PendingResults(Vec::new(), Vec::new()),\n                |mut acc, message| {\n                    if let Message::PendingResults(completed, errors) = &mut acc {\n                        match message {\n                            cosmic::Action::App(Message::PendingComplete(id, selection)) => {\n                                completed.push((id, selection));\n                            }\n                            cosmic::Action::App(Message::PendingError(id, err)) => {\n                                errors.push((id, err));\n                            }\n                            _ => {}\n                        }\n                    }\n                    acc\n                },\n            );\n            cosmic::Action::App(results)\n        })\n    }\n\n    fn handle_completed_operations(\n        &mut self,\n        completed: Vec<(u64, OperationSelection)>,\n    ) -> Task<Message> {\n        let mut commands = Vec::with_capacity(4 * completed.len());\n        let mut op_sel = OperationSelection::default();\n        for (id, op_sel_pending) in completed {\n            op_sel.ignored.extend(op_sel_pending.ignored);\n            op_sel.selected.extend(op_sel_pending.selected);\n            if let Some((op, _)) = self.pending_operations.remove(&id) {\n                // Show toast for some operations\n                if let Some(description) = op.toast() {\n                    if let Operation::Delete { ref paths } = op {\n                        let paths: Arc<[PathBuf]> = Arc::from(paths.as_slice());\n                        commands.push(\n                            self.toasts\n                                .push(\n                                    widget::toaster::Toast::new(description)\n                                        .action(fl!(\"undo\"), move |tid| {\n                                            Message::UndoTrash(tid, paths.clone())\n                                        }),\n                                )\n                                .map(cosmic::Action::App),\n                        );\n                    } else {\n                        commands.push(\n                            self.toasts\n                                .push(widget::toaster::Toast::new(description))\n                                .map(cosmic::Action::App),\n                        );\n                    }\n                }\n\n                // If a favorite for a path has been renamed or moved, update it.\n                if let Operation::Rename { ref from, ref to } = op {\n                    if self.update_favorites([(from, to)].as_slice()) {\n                        commands.push(self.update_config());\n                    }\n                } else if let Operation::Move {\n                    ref paths, ref to, ..\n                } = op\n                {\n                    let path_changes: Box<[_]> = paths\n                        .iter()\n                        .filter_map(|from| from.file_name().map(|name| (from, to.join(name))))\n                        .collect();\n                    if self.update_favorites(&path_changes) {\n                        commands.push(self.update_config());\n                    }\n                }\n\n                if matches!(op, Operation::RemoveFromRecents { .. }) {\n                    commands.push(self.rescan_recents());\n                }\n\n                self.complete_operations.insert(id, op);\n            }\n        }\n        // Close progress notification if all relevant operations are finished\n        if !self\n            .pending_operations\n            .values()\n            .any(|(op, _)| op.show_progress_notification())\n        {\n            self.progress_operations.clear();\n        }\n        // Potentially show a notification\n        commands.push(self.update_notification());\n        // Rescan and select based on operation\n        commands.push(self.rescan_operation_selection(op_sel));\n        // Manually rescan any trash tabs after any operation is completed\n        commands.push(self.rescan_trash());\n\n        Task::batch(commands)\n    }\n\n    fn handle_operation_errors(&mut self, errors: Vec<(u64, OperationError)>) -> Task<Message> {\n        let mut tasks = Vec::new();\n        let mut failed = Vec::new();\n        for (id, err) in errors.into_iter() {\n            if let Some((op, controller)) = self.pending_operations.remove(&id) {\n                // Only show dialog if not cancelled\n                if !controller.is_cancelled() {\n                    match err.kind {\n                        OperationErrorType::Generic(_) => failed.push(id),\n                        OperationErrorType::PasswordRequired => {\n                            tasks.push(self.dialog_pages.push_back(DialogPage::ExtractPassword {\n                                id,\n                                password: String::new(),\n                            }));\n                        }\n                    }\n                }\n\n                // Remove from progress\n                self.progress_operations.remove(&id);\n                self.failed_operations\n                    .insert(id, (op, controller, err.to_string()));\n            }\n        }\n        if !failed.is_empty() {\n            tasks.push(\n                self.dialog_pages\n                    .push_back(DialogPage::FailedOperations(failed)),\n            );\n            tasks.push(widget::text_input::focus(self.dialog_text_input.clone()));\n        }\n\n        // Close progress notification if all relevant operations are finished\n        if !self\n            .pending_operations\n            .values()\n            .any(|(op, _)| op.show_progress_notification())\n        {\n            self.progress_operations.clear();\n        }\n        // Manually rescan any trash tabs after any operation is completed\n        tasks.push(self.rescan_trash());\n        Task::batch(tasks)\n    }\n\n    fn remove_window(&mut self, id: &window::Id) {\n        if let Some(window) = self.windows.remove(id) {\n            match window.kind {\n                WindowKind::ContextMenu(entity, _) => {\n                    // Close context menu\n                    if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                        tab.context_menu = None;\n                    }\n                }\n                WindowKind::Desktop(entity) => {\n                    // Remove the tab from the tab model\n                    self.tab_model.remove(entity);\n                }\n                _ => {}\n            }\n        }\n    }\n\n    fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task<Message> {\n        log::info!(\"rescan_operation_selection {op_sel:?}\");\n        let entity = self.tab_model.active();\n        let Some(tab) = self.tab_model.data::<Tab>(entity) else {\n            return Task::none();\n        };\n        let Some(items) = tab.items_opt() else {\n            return Task::none();\n        };\n        for item in items {\n            if item.selected {\n                if let Some(path) = item.path_opt()\n                    && (op_sel.selected.contains(path) || op_sel.ignored.contains(path))\n                {\n                    // Ignore if path in selected or ignored paths\n                    continue;\n                }\n\n                // Return if there is a previous selection not matching\n                return Task::none();\n            }\n        }\n        self.update_tab(entity, tab.location.clone(), Some(op_sel.selected))\n    }\n\n    fn update_tab(\n        &mut self,\n        entity: Entity,\n        location: Location,\n        selection_paths: Option<Vec<PathBuf>>,\n    ) -> Task<Message> {\n        if let Location::Search(_, term, ..) = location {\n            self.search_set(entity, Some(term), selection_paths)\n        } else {\n            self.rescan_tab(entity, location, selection_paths)\n        }\n    }\n\n    fn rescan_tab(\n        &mut self,\n        entity: Entity,\n        location: Location,\n        selection_paths: Option<Vec<PathBuf>>,\n    ) -> Task<Message> {\n        log::info!(\"rescan_tab {entity:?} {location:?} {selection_paths:?}\");\n        let icon_sizes = self.config.tab.icon_sizes;\n        let mounter_items = self.mounter_items.clone();\n\n        Task::future(async move {\n            let location2 = location.clone();\n            match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {\n                Ok((parent_item_opt, mut items)) => {\n                    #[cfg(feature = \"gvfs\")]\n                    {\n                        let mounter_paths: Box<[_]> = mounter_items\n                            .values()\n                            .flatten()\n                            .filter_map(MounterItem::path)\n                            .collect();\n                        if !mounter_paths.is_empty() {\n                            for item in &mut items {\n                                item.is_mount_point =\n                                    item.path_opt().is_some_and(|p| mounter_paths.contains(p));\n                            }\n                        }\n                    }\n\n                    cosmic::action::app(Message::TabRescan(\n                        entity,\n                        location,\n                        parent_item_opt,\n                        items,\n                        selection_paths,\n                    ))\n                }\n                Err(err) => {\n                    log::warn!(\"failed to rescan: {err}\");\n                    cosmic::action::none()\n                }\n            }\n        })\n    }\n\n    fn rescan_trash(&mut self) -> Task<Message> {\n        let needs_reload: Box<[_]> = self\n            .tab_model\n            .iter()\n            .filter_map(|entity| {\n                let tab = self.tab_model.data::<Tab>(entity)?;\n                tab.location\n                    .is_trash()\n                    .then_some((entity, tab.location.clone()))\n            })\n            .collect();\n\n        let commands = needs_reload\n            .into_iter()\n            .map(|(entity, location)| self.update_tab(entity, location, None));\n\n        Task::batch(commands)\n    }\n\n    fn rescan_recents(&mut self) -> Task<Message> {\n        let needs_reload: Box<[_]> = self\n            .tab_model\n            .iter()\n            .filter_map(|entity| {\n                let tab = self.tab_model.data::<Tab>(entity)?;\n                tab.location\n                    .is_recents()\n                    .then_some((entity, tab.location.clone()))\n            })\n            .collect();\n\n        let commands = needs_reload\n            .into_iter()\n            .map(|(entity, location)| self.update_tab(entity, location, None));\n\n        Task::batch(commands)\n    }\n\n    fn search_get(&self) -> Option<&str> {\n        let entity = self.tab_model.active();\n        let tab = self.tab_model.data::<Tab>(entity)?;\n        match &tab.location {\n            Location::Search(_, term, ..) => Some(term),\n            _ => None,\n        }\n    }\n\n    fn search_set_active(&mut self, term_opt: Option<String>) -> Task<Message> {\n        let entity = self.tab_model.active();\n        self.search_set(entity, term_opt, None)\n    }\n\n    fn search_set(\n        &mut self,\n        tab: Entity,\n        term_opt: Option<String>,\n        selection_paths: Option<Vec<PathBuf>>,\n    ) -> Task<Message> {\n        let mut title_location_opt = None;\n        if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) {\n            let location_opt = match term_opt {\n                Some(term) => {\n                    let search_location = if let Some(path) = tab.location.path_opt() {\n                        Some(SearchLocation::Path(path.clone()))\n                    } else if tab.location.is_recents() {\n                        Some(SearchLocation::Recents)\n                    } else if tab.location.is_trash() {\n                        Some(SearchLocation::Trash)\n                    } else {\n                        None\n                    };\n\n                    search_location.map(|search_location| {\n                        (\n                            Location::Search(\n                                search_location,\n                                term,\n                                tab.config.show_hidden,\n                                Instant::now(),\n                            ),\n                            true,\n                        )\n                    })\n                }\n                None => match &tab.location {\n                    Location::Search(search_location, ..) => match search_location {\n                        SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)),\n                        SearchLocation::Recents => Some((Location::Recents, false)),\n                        SearchLocation::Trash => Some((Location::Trash, false)),\n                    },\n                    _ => None,\n                },\n            };\n            if let Some((location, focus_search)) = location_opt {\n                tab.change_location(&location, None);\n                title_location_opt = Some((tab.title(), tab.location.clone(), focus_search));\n            }\n        }\n        if let Some((title, location, focus_search)) = title_location_opt {\n            self.tab_model.text_set(tab, title);\n            return Task::batch([\n                self.update_title(),\n                self.update_watcher(),\n                self.rescan_tab(tab, location, selection_paths),\n                if focus_search {\n                    widget::text_input::focus(self.search_id.clone())\n                } else {\n                    Task::none()\n                },\n            ]);\n        }\n        Task::none()\n    }\n\n    fn selected_paths(\n        &self,\n        entity_opt: Option<Entity>,\n    ) -> impl Iterator<Item = PathBuf> + use<'_> {\n        let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n        self.tab_model\n            .data::<Tab>(entity)\n            .into_iter()\n            .flat_map(|tab| {\n                tab.selected_locations()\n                    .into_iter()\n                    .filter_map(Location::into_path_opt)\n            })\n    }\n\n    fn set_cut(&mut self, entity_opt: Option<Entity>) {\n        let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n        if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n            tab.cut_selected();\n        }\n    }\n\n    fn update_config(&mut self) -> Task<Message> {\n        self.update_nav_model();\n        // Tabs are collected first to placate the borrowck\n        let tabs: Box<[_]> = self.tab_model.iter().collect();\n        // Update main conf and each tab with the new config\n        let commands = std::iter::once(cosmic::command::set_theme(self.config.app_theme.theme()))\n            .chain(tabs.into_iter().map(|entity| {\n                self.update(Message::TabMessage(\n                    Some(entity),\n                    tab::Message::Config(self.config.tab),\n                ))\n            }));\n        Task::batch(commands)\n    }\n\n    fn update_desktop(&mut self) -> Task<Message> {\n        let needs_reload: Box<[_]> = (self.tab_model.iter())\n            .filter_map(|entity| {\n                let tab = self.tab_model.data::<Tab>(entity)?;\n                if let Location::Desktop(path, output, _) = &tab.location {\n                    Some((\n                        entity,\n                        Location::Desktop(path.clone(), output.clone(), self.config.desktop),\n                    ))\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        let mut commands = Vec::with_capacity(needs_reload.len());\n        for (entity, location) in needs_reload {\n            if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                tab.location = location.clone();\n            }\n            commands.push(self.update_tab(entity, location, None));\n        }\n        Task::batch(commands)\n    }\n\n    fn activate_nav_model_location(&mut self, location: &Location) {\n        let nav_bar_id = self.nav_model.iter().find(|&id| {\n            self.nav_model\n                .data::<Location>(id)\n                .is_some_and(|l| l == location)\n        });\n\n        if let Some(id) = nav_bar_id {\n            self.nav_model.activate(id);\n        } else {\n            let active = self.nav_model.active();\n            segmented_button::Selectable::deactivate(&mut self.nav_model, active);\n        }\n    }\n\n    fn close_context_menus(&mut self) -> Task<Message> {\n        let active = self.tab_model.active();\n        if let Some(tab) = self.tab_model.data_mut::<Tab>(active) {\n            tab.location_context_menu_index = None;\n            if tab.context_menu.is_some() {\n                return self.update(Message::TabMessage(\n                    Some(active),\n                    tab::Message::ContextMenu(None, None),\n                ));\n            }\n        }\n\n        Task::none()\n    }\n\n    fn update_nav_model(&mut self) {\n        let mut nav_model = segmented_button::ModelBuilder::default();\n\n        if self.config.show_recents {\n            nav_model = nav_model.insert(|b| {\n                b.text(fl!(\"recents\"))\n                    .icon(icon::from_name(\"document-open-recent-symbolic\"))\n                    .data(Location::Recents)\n            });\n        }\n\n        for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {\n            if let Some(path) = favorite.path_opt() {\n                let name = if matches!(favorite, Favorite::Home) {\n                    fl!(\"home\")\n                } else if let Favorite::Network { name, .. } = favorite {\n                    name.clone()\n                } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {\n                    file_name.to_string()\n                } else {\n                    fl!(\"filesystem\")\n                };\n                nav_model = nav_model.insert(move |b| {\n                    b.text(name.clone())\n                        .icon(\n                            icon::icon(if path.is_dir() {\n                                tab::folder_icon_symbolic(&path, 16)\n                            } else {\n                                icon::from_name(\"text-x-generic-symbolic\").size(16).handle()\n                            })\n                            .size(16),\n                        )\n                        .data(match favorite {\n                            Favorite::Network { uri, name, path } => {\n                                Location::Network(uri.clone(), name.clone(), Some(path.to_owned()))\n                            }\n                            _ => Location::Path(path.clone()),\n                        })\n                        .data(FavoriteIndex(favorite_i))\n                });\n            }\n        }\n\n        nav_model = nav_model.insert(|b| {\n            b.text(fl!(\"trash\"))\n                .icon(icon::icon(Trash::icon_symbolic(16)))\n                .data(Location::Trash)\n                .divider_above()\n        });\n\n        if !MOUNTERS.is_empty() {\n            nav_model = nav_model.insert(|b| {\n                b.text(fl!(\"networks\"))\n                    .icon(icon::icon(\n                        icon::from_name(\"network-workgroup-symbolic\")\n                            .size(16)\n                            .handle(),\n                    ))\n                    .data(Location::Network(\n                        \"network:///\".to_string(),\n                        fl!(\"networks\"),\n                        None,\n                    ))\n                    .divider_above()\n            });\n        }\n\n        // Collect all mounter items\n        let mut nav_items = Vec::new();\n        for (key, items) in &self.mounter_items {\n            nav_items.extend(items.iter().map(|item| (*key, item)));\n        }\n        // Sort by name lexically\n        nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));\n        // Add items to nav model\n        for (i, (key, item)) in nav_items.into_iter().enumerate() {\n            nav_model = nav_model.insert(|mut b| {\n                b = b.text(item.name()).data(MounterData(key, item.clone()));\n                let uri = item.uri();\n                if let Some(path) = item.path() {\n                    if item.is_remote() {\n                        b = b.data(Location::Network(uri, item.name(), Some(path)));\n                    } else {\n                        b = b.data(Location::Path(path));\n                    }\n                } else if !uri.is_empty() {\n                    b = b.data(Location::Network(uri, item.name(), None));\n                }\n                if let Some(icon) = item.icon(true) {\n                    b = b.icon(icon::icon(icon).size(16));\n                }\n                if item.is_mounted() {\n                    b = b.closable();\n                }\n                if i == 0 {\n                    b = b.divider_above();\n                }\n                b\n            });\n        }\n\n        self.nav_model = nav_model.build();\n\n        let tab_entity = self.tab_model.active();\n        if let Some(tab) = self.tab_model.data::<Tab>(tab_entity) {\n            self.activate_nav_model_location(&tab.location.clone());\n        }\n    }\n\n    fn update_notification(&mut self) -> Task<Message> {\n        // Handle closing notification if there are no operations\n        if self.pending_operations.is_empty() {\n            #[cfg(feature = \"notify\")]\n            if let Some(notification_arc) = self.notification_opt.take() {\n                return Task::future(async move {\n                    tokio::task::spawn_blocking(move || {\n                        //TODO: this is nasty\n                        let notification_mutex = Arc::try_unwrap(notification_arc).unwrap();\n                        let notification = notification_mutex.into_inner().unwrap();\n                        notification.close();\n                    })\n                    .await\n                    .unwrap();\n                    cosmic::action::app(Message::MaybeExit)\n                });\n            }\n        }\n\n        Task::none()\n    }\n\n    fn update_title(&mut self) -> Task<Message> {\n        let window_title = match self.tab_model.text(self.tab_model.active()) {\n            Some(tab_title) => format!(\"{tab_title} — {}\", fl!(\"cosmic-files\")),\n            None => fl!(\"cosmic-files\"),\n        };\n        if let Some(window_id) = self.core.main_window_id() {\n            self.set_window_title(window_title, window_id)\n        } else {\n            Task::none()\n        }\n    }\n\n    fn update_watcher(&mut self) -> Task<Message> {\n        if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {\n            let new_paths: FxHashSet<_> = self\n                .tab_model\n                .iter()\n                .filter_map(|entity| {\n                    let tab = self.tab_model.data::<Tab>(entity)?;\n                    tab.location.path_opt().cloned()\n                })\n                .collect();\n\n            // Unwatch paths no longer used\n            for path in &old_paths {\n                if !new_paths.contains(path) {\n                    match watcher.unwatch(path) {\n                        Ok(()) => {\n                            log::debug!(\"unwatching {}\", path.display());\n                        }\n                        Err(err) => {\n                            log::debug!(\"failed to unwatch {}: {}\", path.display(), err);\n                        }\n                    }\n                }\n            }\n\n            // Watch new paths\n            for path in &new_paths {\n                if !old_paths.contains(path) {\n                    match watcher.watch(path, notify::RecursiveMode::NonRecursive) {\n                        Ok(()) => {\n                            log::debug!(\"watching {}\", path.display());\n                        }\n                        Err(err) => {\n                            log::debug!(\"failed to watch {}: {}\", path.display(), err);\n                        }\n                    }\n                }\n            }\n\n            self.watcher_opt = Some((watcher, new_paths));\n        }\n\n        //TODO: should any of this run in a command?\n        Task::none()\n    }\n\n    fn network_drive(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing {\n            space_xxs, space_m, ..\n        } = theme::spacing();\n        let mut table = widget::column::with_capacity(8);\n        for (i, line) in fl!(\"network-drive-schemes\").lines().enumerate() {\n            let mut row = widget::row::with_capacity(2);\n            for part in line.split(',') {\n                row = row.push(\n                    widget::container(if i == 0 {\n                        widget::text::heading(part.to_string())\n                    } else {\n                        widget::text::body(part.to_string())\n                    })\n                    .width(Length::Fill)\n                    .padding(space_xxs),\n                );\n            }\n            table = table.push(row);\n            if i == 0 {\n                table = table.push(widget::divider::horizontal::light());\n            }\n        }\n        widget::column::with_children([\n            widget::text::body(fl!(\"network-drive-description\")).into(),\n            table.into(),\n        ])\n        .spacing(space_m)\n        .into()\n    }\n\n    fn desktop_view_options(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing {\n            space_m, space_l, ..\n        } = theme::spacing();\n        let config = self.config.desktop;\n\n        let show_on_desktop = settings::section()\n            .title(fl!(\"show-on-desktop\"))\n            .add(\n                settings::item::builder(fl!(\"desktop-folder-content\")).toggler(\n                    config.show_content,\n                    move |show_content| {\n                        Message::DesktopConfig(DesktopConfig {\n                            show_content,\n                            ..config\n                        })\n                    },\n                ),\n            )\n            .add(settings::item::builder(fl!(\"mounted-drives\")).toggler(\n                config.show_mounted_drives,\n                move |show_mounted_drives| {\n                    Message::DesktopConfig(DesktopConfig {\n                        show_mounted_drives,\n                        ..config\n                    })\n                },\n            ))\n            .add(settings::item::builder(fl!(\"trash-folder-icon\")).toggler(\n                config.show_trash,\n                move |show_trash| {\n                    Message::DesktopConfig(DesktopConfig {\n                        show_trash,\n                        ..config\n                    })\n                },\n            ));\n\n        let icon_size = config.icon_size;\n        let grid_spacing = config.grid_spacing;\n        let icon_size_and_spacing = settings::section()\n            .title(fl!(\"icon-size-and-spacing\"))\n            .add(\n                settings::item::builder(fl!(\"icon-size\"))\n                    .description(format!(\"{icon_size}%\"))\n                    .control(\n                        widget::slider(50..=500, icon_size.get(), move |new_value| {\n                            Message::DesktopConfig(DesktopConfig {\n                                icon_size: NonZeroU16::new(new_value).unwrap_or(icon_size),\n                                ..config\n                            })\n                        })\n                        .step(25u16),\n                    ),\n            )\n            .add(\n                settings::item::builder(fl!(\"grid-spacing\"))\n                    .description(format!(\"{grid_spacing}%\"))\n                    .control(\n                        widget::slider(50..=500, grid_spacing.get(), move |new_value| {\n                            Message::DesktopConfig(DesktopConfig {\n                                grid_spacing: NonZeroU16::new(new_value).unwrap_or(grid_spacing),\n                                ..config\n                            })\n                        })\n                        .step(25u16),\n                    ),\n            );\n\n        widget::column::with_capacity(2)\n            .padding([0, space_l, space_l, space_l])\n            .spacing(space_m)\n            .push(show_on_desktop)\n            .push(icon_size_and_spacing)\n            .into()\n    }\n\n    fn edit_history(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing { space_m, .. } = theme::spacing();\n\n        let mut children = Vec::new();\n\n        //TODO: get height from theme?\n        let progress_bar_height = Length::Fixed(4.0);\n\n        if !self.pending_operations.is_empty() {\n            let mut section = widget::settings::section().title(fl!(\"pending\"));\n            for (id, (op, controller)) in self.pending_operations.iter().rev() {\n                let progress = controller.progress();\n                section = section.add(widget::column::with_children([\n                    widget::row::with_children([\n                        widget::determinate_linear(progress)\n                            .width(Length::Fill)\n                            .girth(progress_bar_height)\n                            .into(),\n                        if controller.is_paused() {\n                            widget::tooltip(\n                                widget::button::icon(icon::from_name(\n                                    \"media-playback-start-symbolic\",\n                                ))\n                                .on_press(Message::PendingPause(*id, false))\n                                .padding(8),\n                                widget::text::body(fl!(\"resume\")),\n                                widget::tooltip::Position::Top,\n                            )\n                            .into()\n                        } else {\n                            widget::tooltip(\n                                widget::button::icon(icon::from_name(\n                                    \"media-playback-pause-symbolic\",\n                                ))\n                                .on_press(Message::PendingPause(*id, true))\n                                .padding(8),\n                                widget::text::body(fl!(\"pause\")),\n                                widget::tooltip::Position::Top,\n                            )\n                            .into()\n                        },\n                        widget::tooltip(\n                            widget::button::icon(icon::from_name(\"window-close-symbolic\"))\n                                .on_press(Message::PendingCancel(*id))\n                                .padding(8),\n                            widget::text::body(fl!(\"cancel\")),\n                            widget::tooltip::Position::Top,\n                        )\n                        .into(),\n                    ])\n                    .align_y(Alignment::Center)\n                    .into(),\n                    widget::text::body(op.pending_text(progress, controller.state())).into(),\n                ]));\n            }\n            children.push(section.into());\n        }\n\n        if !self.failed_operations.is_empty() {\n            let mut section = widget::settings::section().title(fl!(\"failed\"));\n            for (op, controller, error) in self.failed_operations.values().rev() {\n                let progress = controller.progress();\n                section = section.add(widget::column::with_children([\n                    widget::text::body(op.pending_text(progress, controller.state())).into(),\n                    widget::text::body(error).into(),\n                ]));\n            }\n            children.push(section.into());\n        }\n\n        if !self.complete_operations.is_empty() {\n            let mut section = widget::settings::section().title(fl!(\"complete\"));\n            for op in self.complete_operations.values().rev() {\n                section = section.add(widget::text::body(op.completed_text()));\n            }\n            children.push(section.into());\n        }\n\n        if children.is_empty() {\n            children.push(widget::text::body(fl!(\"no-history\")).into());\n        }\n\n        widget::column::with_children(children)\n            .spacing(space_m)\n            .into()\n    }\n\n    fn preview<'a>(\n        &'a self,\n        entity_opt: &Option<Entity>,\n        kind: &'a PreviewKind,\n        context_drawer: bool,\n    ) -> Element<'a, tab::Message> {\n        let cosmic_theme::Spacing { space_l, .. } = theme::spacing();\n\n        let mut children = Vec::with_capacity(1);\n        let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n        let military_time = self.config.tab.military_time;\n        match kind {\n            PreviewKind::Custom(PreviewItem(item)) => {\n                children.push(item.preview_view(Some(&self.mime_app_cache), military_time));\n            }\n            PreviewKind::Location(location) => {\n                if let Some(tab) = self.tab_model.data::<Tab>(entity)\n                    && let Some(items) = tab.items_opt()\n                {\n                    for item in items {\n                        if item.location_opt.as_ref() == Some(location) {\n                            children\n                                .push(item.preview_view(Some(&self.mime_app_cache), military_time));\n                            // Only show one property view to avoid issues like hangs when generating\n                            // preview images on thousands of files\n                            break;\n                        }\n                    }\n                }\n            }\n            PreviewKind::Selected => {\n                if let Some(tab) = self.tab_model.data::<Tab>(entity)\n                    && let Some(items) = tab.items_opt()\n                {\n                    let preview_opt = {\n                        let mut selected = items.iter().filter(|item| item.selected);\n\n                        match (selected.next(), selected.next()) {\n                            // At least two selected items\n                            (Some(_), Some(_)) => {\n                                Some(tab.multi_preview_view(Some(&self.mime_app_cache)))\n                            }\n                            // Exactly one selected item\n                            (Some(item), None) => {\n                                Some(item.preview_view(Some(&self.mime_app_cache), military_time))\n                            }\n                            // No selected items\n                            _ => None,\n                        }\n                    };\n\n                    if let Some(preview) = preview_opt {\n                        children.push(preview);\n                    }\n\n                    if children.is_empty()\n                        && let Some(item) = &tab.parent_item_opt\n                    {\n                        children.push(item.preview_view(Some(&self.mime_app_cache), military_time));\n                    }\n                }\n            }\n        }\n        widget::column::with_children(children)\n            .padding(if context_drawer {\n                [0, 0, 0, 0]\n            } else {\n                [0, space_l, space_l, space_l]\n            })\n            .into()\n    }\n\n    fn settings(&self) -> Element<'_, Message> {\n        let tab_config = self.config.tab;\n\n        // TODO: Should dialog be updated here too?\n        settings::view_column(vec![\n            settings::section()\n                .title(fl!(\"appearance\"))\n                .add({\n                    let app_theme_selected = match self.config.app_theme {\n                        AppTheme::Dark => 1,\n                        AppTheme::Light => 2,\n                        AppTheme::System => 0,\n                    };\n                    settings::item::builder(fl!(\"theme\")).control(widget::dropdown(\n                        &self.app_themes,\n                        Some(app_theme_selected),\n                        move |index| {\n                            Message::AppTheme(match index {\n                                1 => AppTheme::Dark,\n                                2 => AppTheme::Light,\n                                _ => AppTheme::System,\n                            })\n                        },\n                    ))\n                })\n                .into(),\n            settings::section()\n                .title(fl!(\"type-to-search\"))\n                .add(\n                    settings::item::builder(fl!(\"type-to-search-recursive\")).radio(\n                        TypeToSearch::Recursive,\n                        Some(self.config.type_to_search),\n                        Message::SetTypeToSearch,\n                    ),\n                )\n                .add(\n                    settings::item::builder(fl!(\"type-to-search-enter-path\")).radio(\n                        TypeToSearch::EnterPath,\n                        Some(self.config.type_to_search),\n                        Message::SetTypeToSearch,\n                    ),\n                )\n                .add(settings::item::builder(fl!(\"type-to-search-select\")).radio(\n                    TypeToSearch::SelectByPrefix,\n                    Some(self.config.type_to_search),\n                    Message::SetTypeToSearch,\n                ))\n                .into(),\n            settings::section()\n                .title(fl!(\"other\"))\n                .add({\n                    settings::item::builder(fl!(\"single-click\")).toggler(\n                        tab_config.single_click,\n                        move |single_click| {\n                            Message::TabConfig(TabConfig {\n                                single_click,\n                                ..tab_config\n                            })\n                        },\n                    )\n                })\n                .add({\n                    settings::item::builder(fl!(\"show-recents\"))\n                        .toggler(self.config.show_recents, Message::SetShowRecents)\n                })\n                .into(),\n        ])\n        .into()\n    }\n\n    fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {\n        let mut results = Vec::new();\n\n        let mut dedupe = FxHashSet::default();\n\n        // start with exact matches\n        results.extend(\n            self.mime_app_cache\n                .get(mime_type)\n                .iter()\n                .filter(|&mime_app| dedupe.insert(&mime_app.id))\n                .map(|mime_app| (mime_app, MimeAppMatch::Exact)),\n        );\n\n        // grab matches based off of subclass / parent mime type\n        if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) {\n            for parent_type in parent_types {\n                results.extend(\n                    self.mime_app_cache\n                        .get(&parent_type)\n                        .iter()\n                        .filter(|&mime_app| dedupe.insert(&mime_app.id))\n                        .map(|mime_app| (mime_app, MimeAppMatch::Related)),\n                );\n            }\n        }\n\n        // Add other apps\n        results.extend(\n            self.mime_app_cache\n                .apps()\n                .iter()\n                .filter(|&mime_app| dedupe.insert(&mime_app.id))\n                .map(|mime_app| (mime_app, MimeAppMatch::Other)),\n        );\n\n        results\n    }\n\n    // Update favorites based on renaming or moving dirs.\n    fn update_favorites(&mut self, path_changes: &[(impl AsRef<Path>, impl AsRef<Path>)]) -> bool {\n        let mut favorites_changed = false;\n        let favorites = self\n            .config\n            .favorites\n            .iter()\n            .map(|favorite| {\n                if let Favorite::Path(path) = favorite {\n                    for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) {\n                        if path.starts_with(from)\n                            && let Ok(relative) = path.strip_prefix(from)\n                        {\n                            favorites_changed = true;\n                            return Favorite::from_path(to.join(relative));\n                        }\n                    }\n                }\n                favorite.clone()\n            })\n            .collect();\n\n        if favorites_changed {\n            if let Some(config_handler) = &self.config_handler {\n                match self.config.set_favorites(config_handler, favorites) {\n                    Ok(updated) => {\n                        if updated {\n                            return true;\n                        }\n                    }\n                    Err(err) => {\n                        log::warn!(\"failed to update favorites after moving directories: {err:?}\",);\n                    }\n                }\n            } else {\n                self.config.favorites = favorites;\n                log::warn!(\n                    \"failed to update favorites after moving directories: no config handler\",\n                );\n            }\n        }\n\n        false\n    }\n}\n\n/// Implement [`Application`] to integrate with COSMIC.\nimpl Application for App {\n    /// Default async executor to use with the app.\n    type Executor = executor::Default;\n\n    /// Argument received\n    type Flags = Flags;\n\n    /// Message type specific to our [`App`].\n    type Message = Message;\n\n    /// The unique application ID to supply to the window manager.\n    const APP_ID: &'static str = \"com.system76.CosmicFiles\";\n\n    fn core(&self) -> &Core {\n        &self.core\n    }\n\n    fn core_mut(&mut self) -> &mut Core {\n        &mut self.core\n    }\n\n    /// Creates the application, and optionally emits command on initialize.\n    fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {\n        core.window.context_is_overlay = false;\n        match flags.mode {\n            Mode::App => {\n                core.window.show_context = flags.config.show_details;\n            }\n            Mode::Desktop => {\n                core.window.content_container = false;\n                core.window.show_window_menu = false;\n                core.window.show_headerbar = false;\n                core.window.sharp_corners = false;\n                core.window.show_maximize = false;\n                core.window.show_minimize = false;\n                core.window.use_template = true;\n            }\n        }\n\n        let app_themes = vec![fl!(\"match-desktop\"), fl!(\"dark\"), fl!(\"light\")];\n\n        let key_binds = key_binds(&match flags.mode {\n            Mode::App => tab::Mode::App,\n            Mode::Desktop => tab::Mode::Desktop,\n        });\n\n        // Create a dedicated thread for the compio runtime to handle operations on.\n        // Supports io_uring on Linux, IOPC on Windows, and polling everywhere else.\n        let (compio_tx, mut compio_rx) = mpsc::channel(1);\n        let tokio_handle = tokio::runtime::Handle::current();\n        std::thread::spawn(move || {\n            let _tokio = tokio_handle.enter();\n            compio::runtime::RuntimeBuilder::new()\n                .build()\n                .unwrap()\n                .block_on(async move {\n                    while let Some(task) = compio_rx.recv().await {\n                        compio::runtime::spawn(task).detach();\n                    }\n                });\n        });\n\n        let about = About::default()\n            .name(fl!(\"cosmic-files\"))\n            .icon(icon::from_name(Self::APP_ID))\n            .version(env!(\"CARGO_PKG_VERSION\"))\n            .author(\"System76\")\n            .comments(fl!(\"comment\"))\n            .license(\"GPL-3.0-only\")\n            .license_url(\"https://spdx.org/licenses/GPL-3.0-only\")\n            .developers([(\"Jeremy Soller\", \"jeremy@system76.com\")])\n            .links([\n                (fl!(\"repository\"), \"https://github.com/pop-os/cosmic-files\"),\n                (\n                    fl!(\"support\"),\n                    \"https://github.com/pop-os/cosmic-files/issues\",\n                ),\n            ]);\n\n        let mut app = Self {\n            core,\n            about,\n            nav_bar_context_id: segmented_button::Entity::null(),\n            nav_model: segmented_button::ModelBuilder::default().build(),\n            tab_model: segmented_button::ModelBuilder::default().build(),\n            config_handler: flags.config_handler,\n            state_handler: flags.state_handler,\n            config: flags.config,\n            state: flags.state,\n            mode: flags.mode,\n            app_themes,\n            compio_tx,\n            context_page: ContextPage::Preview(None, PreviewKind::Selected),\n            dialog_pages: DialogPages::new(),\n            dialog_text_input: widget::Id::new(\"Dialog Text Input\"),\n            key_binds,\n            margin: FxHashMap::default(),\n            mime_app_cache: MimeAppCache::new(),\n            modifiers: Modifiers::empty(),\n            mounter_items: FxHashMap::default(),\n            must_save_sort_names: false,\n            network_drive_connecting: None,\n            network_drive_input: String::new(),\n            #[cfg(feature = \"notify\")]\n            notification_opt: None,\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            overlap: FxHashMap::default(),\n            pending_operation_id: 0,\n            pending_operations: BTreeMap::new(),\n            progress_operations: BTreeSet::new(),\n            complete_operations: BTreeMap::new(),\n            failed_operations: BTreeMap::new(),\n            scrollable_id: widget::Id::new(\"File Scrollable\"),\n            search_id: widget::Id::new(\"File Search\"),\n            size: None,\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            surface_ids: FxHashMap::default(),\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            surface_names: FxHashMap::default(),\n            toasts: widget::toaster::Toasts::new(Message::CloseToast),\n            watcher_opt: None,\n            windows: FxHashMap::default(),\n            nav_dnd_hover: None,\n            tab_dnd_hover: None,\n            type_select_prefix: String::new(),\n            type_select_last_key: None,\n            nav_drag_id: DragId::new(),\n            tab_drag_id: DragId::new(),\n            auto_scroll_speed: None,\n            file_dialog_opt: None,\n            clipboard_cache: ClipboardCache::Empty,\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            layer_sizes: FxHashMap::default(),\n        };\n\n        let mut commands = vec![app.update_config(), app.update(Message::CheckClipboard)];\n\n        for location in flags.locations {\n            if let Some(path) = location.path_opt()\n                && path.is_file()\n                && let Some(parent) = path.parent()\n            {\n                commands.push(app.open_tab(\n                    Location::Path(parent.to_path_buf()),\n                    true,\n                    Some(vec![path.clone()]),\n                ));\n                continue;\n            }\n            commands.push(app.open_tab(location, true, None));\n        }\n        for location in flags.uris {\n            if let Some(e) = app.nav_model.iter().find(|e| {\n                app.nav_model.data::<Location>(*e).is_some_and(\n                    |l| matches!(l, Location::Network(uri, ..) if *uri == *location.as_str()),\n                )\n            }) {\n                commands.push(cosmic::task::message(cosmic::Action::App(\n                    Message::NetworkDriveOpenEntityAfterMount { entity: e },\n                )));\n            }\n        }\n\n        if app.tab_model.entity_at(0).is_none() {\n            if let Ok(current_dir) = env::current_dir() {\n                commands.push(app.open_tab(Location::Path(current_dir), true, None));\n            } else {\n                commands.push(app.open_tab(Location::Path(home_dir()), true, None));\n            }\n        }\n\n        (app, Task::batch(commands))\n    }\n\n    fn nav_bar(&self) -> Option<Element<'_, cosmic::Action<Self::Message>>> {\n        if !self.core.nav_bar_active() {\n            return None;\n        }\n\n        let nav_model = self.nav_model()?;\n\n        let mut nav = cosmic::widget::nav_bar(nav_model, |entity| {\n            cosmic::Action::Cosmic(cosmic::app::Action::NavBar(entity))\n        })\n        .drag_id(self.nav_drag_id)\n        .on_dnd_enter(|entity, _| cosmic::Action::App(Message::DndEnterNav(entity)))\n        .on_dnd_leave(|_| cosmic::Action::App(Message::DndExitNav))\n        .on_dnd_drop(|entity, data, action| {\n            cosmic::Action::App(Message::DndDropNav(entity, data, action))\n        })\n        .on_context(|entity| cosmic::Action::App(Message::NavBarContext(entity)))\n        .on_close(|entity| cosmic::Action::App(Message::NavBarClose(entity)))\n        .on_middle_press(|entity| {\n            cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity)))\n        })\n        .context_menu(self.nav_context_menu(self.nav_bar_context_id))\n        .close_icon(icon::from_name(\"media-eject-symbolic\").size(16).icon())\n        .into_container();\n\n        if !self.core.is_condensed() {\n            nav = nav.max_width(280);\n        }\n\n        Some(Element::from(\n            nav.width(Length::Shrink).height(Length::Fill),\n        ))\n    }\n\n    fn nav_context_menu(\n        &self,\n        entity: widget::nav_bar::Id,\n    ) -> Option<Vec<widget::menu::Tree<cosmic::Action<Self::Message>>>> {\n        let favorite_index_opt = self.nav_model.data::<FavoriteIndex>(entity);\n        let location_opt = self.nav_model.data::<Location>(entity);\n\n        let mut items = Vec::with_capacity(7);\n\n        if location_opt\n            .and_then(Location::path_opt)\n            .is_some_and(|x| x.is_file())\n        {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"open\"),\n                None,\n                NavMenuAction::Open(entity),\n            ));\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"menu-open-with\"),\n                None,\n                NavMenuAction::OpenWith(entity),\n            ));\n        } else {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"open-in-new-tab\"),\n                None,\n                NavMenuAction::OpenInNewTab(entity),\n            ));\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"open-in-new-window\"),\n                None,\n                NavMenuAction::OpenInNewWindow(entity),\n            ));\n        }\n        if let Some(path) = location_opt.and_then(Location::path_opt) {\n            let selected_dir = usize::from(path.is_dir());\n            let action_items: Vec<_> = self\n                .config\n                .context_actions\n                .iter()\n                .enumerate()\n                .filter(|(_, action)| action.matches_selection(1, selected_dir))\n                .map(|(i, action)| {\n                    cosmic::widget::menu::Item::Button(\n                        action.name.clone(),\n                        None,\n                        NavMenuAction::RunContextAction(entity, i),\n                    )\n                })\n                .collect();\n\n            if !action_items.is_empty() {\n                items.push(cosmic::widget::menu::Item::Divider);\n                items.extend(action_items);\n            }\n        }\n        items.push(cosmic::widget::menu::Item::Divider);\n        if matches!(location_opt, Some(Location::Path(..))) {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"show-details\"),\n                None,\n                NavMenuAction::Preview(entity),\n            ));\n        }\n        items.push(cosmic::widget::menu::Item::Divider);\n        if favorite_index_opt.is_some() {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"remove-from-sidebar\"),\n                None,\n                NavMenuAction::RemoveFromSidebar(entity),\n            ));\n        }\n\n        if matches!(location_opt, Some(Location::Recents)) && tab::has_recents() {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"clear-recents-history\"),\n                None,\n                NavMenuAction::ClearRecents,\n            ));\n        }\n\n        if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() {\n            items.push(cosmic::widget::menu::Item::Button(\n                fl!(\"empty-trash\"),\n                None,\n                NavMenuAction::EmptyTrash,\n            ));\n        }\n\n        Some(cosmic::widget::menu::items(&HashMap::new(), items))\n    }\n\n    fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {\n        match self.mode {\n            Mode::App => Some(&self.nav_model),\n            Mode::Desktop => None,\n        }\n    }\n\n    fn on_nav_select(&mut self, entity: Entity) -> Task<Self::Message> {\n        self.nav_model.activate(entity);\n        if let Some(location) = self.nav_model.data::<Location>(entity) {\n            let should_open = match location {\n                #[cfg(feature = \"gvfs\")]\n                Location::Network(uri, name, Some(path))\n                    if !path.try_exists().unwrap_or_default() =>\n                {\n                    let mut found = false;\n\n                    if let Some(key) = self\n                        .mounter_items\n                        .iter()\n                        .find_map(|(k, items)| {\n                            items.iter().find_map(|item| {\n                                found |= item.path().is_some_and(|p| path.starts_with(&p))\n                                    || item.name() == *name\n                                    || item.uri() == *uri;\n                                (!item.is_mounted() && found).then_some(*k)\n                            })\n                        })\n                        .or(if found {\n                            None\n                        } else {\n                            // TODO do we need to choose the correct mounter?\n                            self.mounter_items.keys().copied().next()\n                        })\n                        && let Some(mounter) = MOUNTERS.get(&key)\n                    {\n                        return mounter.network_drive(uri.clone()).map(move |()| {\n                            cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount {\n                                entity,\n                            })\n                        });\n                    }\n\n                    log::warn!(\n                        \"failed to open favorite, path does not exist: {}\",\n                        path.display()\n                    );\n                    return self.push_dialog(\n                        DialogPage::FavoritePathError {\n                            path: path.clone(),\n                            entity,\n                        },\n                        Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()),\n                    );\n                }\n                Location::Path(path) | Location::Network(_, _, Some(path)) => {\n                    match path.try_exists() {\n                        Ok(true) => true,\n                        Ok(false) => {\n                            log::warn!(\n                                \"failed to open favorite, path does not exist: {}\",\n                                path.display()\n                            );\n                            return self.push_dialog(\n                                DialogPage::FavoritePathError {\n                                    path: path.clone(),\n                                    entity,\n                                },\n                                Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()),\n                            );\n                        }\n                        Err(err) => {\n                            log::warn!(\n                                \"failed to open favorite for path: {}, {}\",\n                                path.display(),\n                                err\n                            );\n                            return self.push_dialog(\n                                DialogPage::FavoritePathError {\n                                    path: path.clone(),\n                                    entity,\n                                },\n                                Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()),\n                            );\n                        }\n                    }\n                }\n\n                _ => true,\n            };\n\n            if should_open {\n                let message = Message::TabMessage(None, tab::Message::Location(location.clone()));\n                return self.update(message);\n            }\n        }\n        if let Some(data) = self.nav_model.data::<MounterData>(entity)\n            && let Some(mounter) = MOUNTERS.get(&data.0)\n        {\n            return mounter\n                .mount(data.1.clone())\n                .map(|()| cosmic::action::none());\n        }\n        Task::none()\n    }\n\n    fn on_app_exit(&mut self) -> Option<Message> {\n        Some(Message::WindowClose)\n    }\n\n    fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {\n        Some(Message::WindowCloseRequested(id))\n    }\n\n    fn on_context_drawer(&mut self) -> Task<Self::Message> {\n        if let ContextPage::Preview(..) = self.context_page {\n            // Persist state of preview page\n            if self.core.window.show_context != self.config.show_details {\n                return self.update(Message::Preview(None));\n            }\n        }\n        Task::none()\n    }\n\n    fn on_escape(&mut self) -> Task<Self::Message> {\n        let entity = self.tab_model.active();\n\n        // Close dialog if open\n        if let Some((_page, task)) = self.dialog_pages.pop_front() {\n            return task;\n        }\n\n        // Close gallery mode if open\n        if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n            && tab.gallery\n        {\n            tab.gallery = false;\n            return Task::none();\n        }\n\n        // Close menus and context panes in order per message\n        // Why: It'd be weird to close everything all at once\n        // Usually, the Escape key (for example) closes menus and panes one by one instead\n        // of closing everything on one press\n        if self.core.window.show_context {\n            self.set_show_context(false);\n            return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false)));\n        }\n        if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n            if tab.location_context_menu_index.is_some() {\n                tab.location_context_menu_index = None;\n                return Task::none();\n            }\n\n            if tab.context_menu.is_some() {\n                return self.update(Message::TabMessage(\n                    Some(entity),\n                    tab::Message::ContextMenu(None, None),\n                ));\n            }\n\n            if tab.edit_location.is_some() {\n                tab.edit_location = None;\n                return Task::none();\n            }\n\n            let had_focused_button = tab.select_focus_id().is_some();\n            if tab.select_none() {\n                if had_focused_button {\n                    // Unfocus if there was a focused button\n                    return widget::button::focus(widget::Id::unique());\n                }\n                return Task::none();\n            }\n        }\n\n        if self.search_get().is_some() {\n            // Close search if open\n            return self.search_set_active(None);\n        }\n\n        Task::none()\n    }\n\n    /// Handle application events here.\n    fn update(&mut self, message: Self::Message) -> Task<Self::Message> {\n        // Helper for updating config values efficiently\n        macro_rules! config_set {\n            ($name: ident, $value: expr) => {\n                match &self.config_handler {\n                    Some(config_handler) => {\n                        match paste::paste! { self.config.[<set_ $name>](config_handler, $value) } {\n                            Ok(_) => {}\n                            Err(err) => {\n                                log::warn!(\n                                    \"failed to save config {:?}: {}\",\n                                    stringify!($name),\n                                    err\n                                );\n                            }\n                        }\n                    }\n                    None => {\n                        self.config.$name = $value;\n                        log::warn!(\n                            \"failed to save config {:?}: no config handler\",\n                            stringify!($name)\n                        );\n                    }\n                }\n            };\n        }\n\n        match message {\n            Message::AddToSidebar(entity_opt) => {\n                let mut favorites = self.config.favorites.clone();\n                // check if the selected entity is in the current tab\n                // else just use the selected entity and check its location\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n\n                for path in self.selected_paths(entity_opt) {\n                    let is_network = self.tab_model.data::<Tab>(entity).and_then(|tab| {\n                        let in_current_tab = tab\n                            .location\n                            .path_opt()\n                            .zip(path.parent())\n                            .is_some_and(|(t_path, parent)| parent == t_path);\n                        let tab = if in_current_tab {\n                            self.tab_model\n                                .data::<Tab>(self.tab_model.active())\n                                .unwrap_or(tab)\n                        } else {\n                            tab\n                        };\n\n                        let name = Location::Path(path.clone()).title();\n                        if let Location::Network(uri, _, _) = tab\n                            .items_opt\n                            .as_ref()\n                            .and_then(|items| items.iter().find(|&i| i.path_opt() == Some(&path)))\n                            .unwrap()\n                            .location_opt\n                            .as_ref()\n                            .unwrap()\n                        {\n                            Some((uri.clone(), name, path.clone()))\n                        } else {\n                            None\n                        }\n                    });\n                    let name = Location::Path(path.clone()).title();\n                    let favorite = if let Some((uri, _, _)) = is_network.clone() {\n                        Favorite::Network { uri, name, path }\n                    } else {\n                        Favorite::from_path(path)\n                    };\n                    if !favorites.contains(&favorite) {\n                        favorites.push(favorite);\n                    }\n                }\n                config_set!(favorites, favorites);\n                return self.update_config();\n            }\n            Message::AppTheme(app_theme) => {\n                config_set!(app_theme, app_theme);\n                return self.update_config();\n            }\n            Message::Compress(entity_opt) => {\n                let paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                if let Some(current_path) = paths.first()\n                    && let Some(destination) = current_path.parent().zip(current_path.file_stem())\n                {\n                    let to = destination.0.to_path_buf();\n                    let name = destination.1.to_str().unwrap_or_default().to_string();\n                    let archive_type = ArchiveType::default();\n                    return self.push_dialog(\n                        DialogPage::Compress {\n                            paths,\n                            to,\n                            name,\n                            archive_type,\n                            password: None,\n                        },\n                        Some(self.dialog_text_input.clone()),\n                    );\n                }\n            }\n            Message::Config(config) => {\n                if config != self.config {\n                    log::info!(\"update config\");\n                    // Show details is preserved for existing instances\n                    let show_details = self.config.show_details;\n                    self.config = config;\n                    self.config.show_details = show_details;\n                    return self.update_config();\n                }\n            }\n            Message::Copy(entity_opt) => {\n                if let Some(entity) = entity_opt\n                    && let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                {\n                    tab.refresh_cut(&[]);\n                }\n                let paths = self.selected_paths(entity_opt);\n                self.clipboard_cache = ClipboardCache::Files(ClipboardPaste {\n                    paths: paths.map(|p| p.to_path_buf()).collect(),\n                    kind: ClipboardKind::Copy,\n                });\n                let contents =\n                    ClipboardCopy::new(ClipboardKind::Copy, self.selected_paths(entity_opt));\n                return clipboard::write_data(contents);\n            }\n            Message::CopyPath(entity_opt) => {\n                let paths = self.selected_paths(entity_opt);\n                let path_strings: Vec<String> =\n                    paths.into_iter().map(|p| p.display().to_string()).collect();\n                let text = path_strings.join(\"\\n\");\n                return clipboard::write(text);\n            }\n            Message::CopyTo(entity_opt) => {\n                let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                return self.copy_to(&selected_paths);\n            }\n            Message::CopyToResult(result) => {\n                match result {\n                    DialogResult::Cancel => {}\n                    DialogResult::Open(selected_paths) => {\n                        let mut file_paths = None;\n                        if let Some(file_dialog) = &self.file_dialog_opt\n                            && let Some(window) = self.windows.remove(&file_dialog.window_id())\n                            && let WindowKind::FileDialog(paths) = window.kind\n                        {\n                            file_paths = paths;\n                        }\n                        if let Some(file_paths) = file_paths\n                            && !selected_paths.is_empty()\n                        {\n                            self.file_dialog_opt = None;\n                            return self.operation(Operation::Copy {\n                                paths: file_paths.to_vec(),\n                                to: selected_paths[0].clone(),\n                            });\n                        }\n                    }\n                }\n                self.file_dialog_opt = None;\n            }\n            Message::Cut(entity_opt) => {\n                self.set_cut(entity_opt);\n                let paths = self.selected_paths(entity_opt);\n                self.clipboard_cache = ClipboardCache::Files(ClipboardPaste {\n                    paths: paths.map(|p| p.to_path_buf()).collect(),\n                    kind: ClipboardKind::Cut { is_dnd: false },\n                });\n                let contents = ClipboardCopy::new(\n                    ClipboardKind::Cut { is_dnd: false },\n                    self.selected_paths(entity_opt),\n                );\n\n                return clipboard::write_data(contents);\n            }\n            Message::CloseToast(id) => {\n                self.toasts.remove(id);\n            }\n            Message::CosmicSettings(arg) => {\n                //TODO: use special settings URL scheme instead?\n                let mut command = process::Command::new(\"cosmic-settings\");\n                command.arg(arg);\n                match spawn_detached(&mut command) {\n                    Ok(()) => {}\n                    Err(err) => {\n                        log::warn!(\"failed to run cosmic-settings {arg}: {err}\");\n                    }\n                }\n            }\n            Message::Delete(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n                    if tab.location.is_trash() {\n                        if let Some(items) = tab.items_opt() {\n                            let mut trash_items = Vec::new();\n                            for item in items {\n                                if item.selected {\n                                    if let ItemMetadata::Trash { entry, .. } = &item.metadata {\n                                        trash_items.push(entry.clone());\n                                    } else {\n                                        //TODO: error on trying to permanently delete non-trash file?\n                                    }\n                                }\n                            }\n                            if !trash_items.is_empty() {\n                                return self.update(Message::DialogPush(\n                                    DialogPage::DeleteTrash { items: trash_items },\n                                    Some(DELETE_TRASH_BUTTON_ID.clone()),\n                                ));\n                            }\n                        }\n                    } else {\n                        let paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                        if !paths.is_empty() {\n                            return self.delete(paths);\n                        }\n                    }\n                }\n            }\n            Message::DesktopConfig(config) => {\n                if config != self.config.desktop {\n                    config_set!(desktop, config);\n                    return self.update_desktop();\n                }\n            }\n            Message::DesktopViewOptions => {\n                let mut settings = window::Settings {\n                    decorations: true,\n                    min_size: Some(Size::new(360.0, 180.0)),\n                    resizable: true,\n                    size: Size::new(480.0, 444.0),\n                    transparent: true,\n                    ..Default::default()\n                };\n\n                #[cfg(target_os = \"linux\")]\n                {\n                    // Use the dialog ID to make it float\n                    settings.platform_specific.application_id =\n                        \"com.system76.CosmicFilesDialog\".to_string();\n                }\n\n                let (id, command) = window::open(settings);\n                self.windows\n                    .insert(id, Window::new(WindowKind::DesktopViewOptions));\n                return command.map(|_id| cosmic::action::none());\n            }\n            Message::DesktopDialogs(show) => {\n                if matches!(self.mode, Mode::Desktop) {\n                    if show {\n                        //TODO: would it be better to make this a layer surface?\n                        let mut settings = window::Settings {\n                            decorations: false,\n                            level: window::Level::AlwaysOnTop,\n                            max_size: Some(Size::new(1280.0, 640.0)),\n                            min_size: Some(Size::new(320.0, 180.0)),\n                            position: window::Position::Centered,\n                            resizable: false,\n                            size: Size::new(640.0, 320.0),\n                            transparent: true,\n                            ..Default::default()\n                        };\n\n                        #[cfg(target_os = \"linux\")]\n                        {\n                            // Use the dialog ID to make it float\n                            settings.platform_specific.application_id =\n                                \"com.system76.CosmicFilesDialog\".to_string();\n                        }\n\n                        let (id, command) = window::open(settings);\n                        self.windows\n                            .insert(id, Window::new(WindowKind::Dialogs(widget::Id::unique())));\n                        return command.map(|_id| cosmic::Action::None);\n                    }\n\n                    let tasks = self\n                        .windows\n                        .iter()\n                        .filter(|(_, window)| matches!(window.kind, WindowKind::Dialogs(_)))\n                        .map(|(id, _)| window::close(*id));\n                    return Task::batch(tasks);\n                }\n            }\n            Message::DialogCancel => {\n                if let Some((_page, task)) = self.dialog_pages.pop_front() {\n                    return task;\n                }\n            }\n            Message::DialogComplete => {\n                if let Some((dialog_page, task)) = self.dialog_pages.pop_front() {\n                    let mut tasks = vec![task];\n                    match dialog_page {\n                        DialogPage::Compress {\n                            paths,\n                            to,\n                            name,\n                            archive_type,\n                            password,\n                        } => {\n                            let extension = archive_type.extension();\n                            let name = format!(\"{name}{extension}\");\n                            let to = to.join(name);\n                            tasks.push(self.operation(Operation::Compress {\n                                paths: paths.into_vec(),\n                                to,\n                                archive_type,\n                                password,\n                            }));\n                        }\n                        DialogPage::EmptyTrash => {\n                            tasks.push(self.operation(Operation::EmptyTrash));\n                        }\n                        DialogPage::FailedOperation(id) => {\n                            log::warn!(\"TODO: retry operation {id}\");\n                        }\n                        DialogPage::FailedOperations(_ids) => {\n                            log::warn!(\"TODO: retry operations\");\n                        }\n                        DialogPage::ExtractPassword { id, password } => {\n                            let (operation, _, _err) = self.failed_operations.get(&id).unwrap();\n                            let new_op = match &operation {\n                                Operation::Extract { to, paths, .. } => Operation::Extract {\n                                    to: to.clone(),\n                                    paths: paths.clone(),\n                                    password: Some(password),\n                                },\n                                _ => unreachable!(),\n                            };\n                            tasks.push(self.operation(new_op));\n                        }\n                        DialogPage::MountError {\n                            mounter_key,\n                            item,\n                            error: _,\n                        } => {\n                            if let Some(mounter) = MOUNTERS.get(&mounter_key) {\n                                tasks.push(mounter.mount(item).map(|()| cosmic::action::none()));\n                            }\n                        }\n                        DialogPage::NetworkAuth {\n                            mounter_key: _,\n                            uri: _,\n                            auth,\n                            auth_tx,\n                        } => {\n                            tasks.push(Task::future(async move {\n                                auth_tx.send(auth).await.unwrap();\n                                cosmic::action::none()\n                            }));\n                        }\n                        DialogPage::NetworkError {\n                            mounter_key: _,\n                            uri,\n                            error: _,\n                        } => {\n                            //TODO: re-use mounter_key?\n                            tasks.push(self.update(Message::NetworkDriveInput(uri)));\n                            tasks.push(self.update(Message::NetworkDriveSubmit));\n                        }\n                        DialogPage::NewItem { parent, name, dir } => {\n                            let path = parent.join(name);\n                            tasks.push(self.operation(if dir {\n                                Operation::NewFolder { path }\n                            } else {\n                                Operation::NewFile { path }\n                            }));\n                        }\n                        DialogPage::RunContextAction { action, paths } => {\n                            context_action::run(&self.config.context_actions, action, &paths);\n                        }\n                        DialogPage::OpenWith {\n                            path,\n                            mime,\n                            selected,\n                            ..\n                        } => {\n                            let available_apps = self.get_apps_for_mime(&mime);\n\n                            if let Some((app, _)) = available_apps.get(selected) {\n                                if let Some(mut command) =\n                                    app.command(&[&path]).and_then(|v| v.into_iter().next())\n                                {\n                                    match spawn_detached(&mut command) {\n                                        Ok(()) => {\n                                            if self.config.show_recents {\n                                                let _ = recently_used_xbel::update_recently_used(\n                                                    &path,\n                                                    Self::APP_ID.to_string(),\n                                                    \"cosmic-files\".to_string(),\n                                                    None,\n                                                );\n                                            }\n                                        }\n                                        Err(err) => {\n                                            log::warn!(\n                                                \"failed to open {} with {:?}: {}\",\n                                                path.display(),\n                                                app.id,\n                                                err\n                                            );\n                                        }\n                                    }\n                                } else {\n                                    log::warn!(\n                                        \"failed to open {} with {:?}: failed to get command\",\n                                        path.display(),\n                                        app.id\n                                    );\n                                }\n                            }\n                        }\n                        DialogPage::PermanentlyDelete { paths } => {\n                            tasks.push(self.operation(Operation::PermanentlyDelete { paths }));\n                        }\n                        DialogPage::DeleteTrash { items } => {\n                            tasks.push(self.operation(Operation::DeleteTrash { items }));\n                        }\n                        DialogPage::RenameItem {\n                            from, parent, name, ..\n                        } => {\n                            let to = parent.join(name);\n                            tasks.push(self.operation(Operation::Rename { from, to }));\n                        }\n                        DialogPage::Replace { .. } => {\n                            log::warn!(\"replace dialog should be completed with replace result\");\n                        }\n                        DialogPage::SetExecutableAndLaunch { path } => {\n                            tasks.push(self.operation(Operation::SetExecutableAndLaunch { path }));\n                        }\n                        DialogPage::FavoritePathError { entity, .. } => {\n                            if let Some(FavoriteIndex(favorite_i)) =\n                                self.nav_model.data::<FavoriteIndex>(entity)\n                            {\n                                let mut favorites = self.config.favorites.clone();\n                                favorites.remove(*favorite_i);\n                                config_set!(favorites, favorites);\n                                tasks.push(self.update_config());\n                            }\n                        }\n                    }\n                    return Task::batch(tasks);\n                }\n            }\n            Message::DialogPush(dialog_page, focused_id) => {\n                return self.push_dialog(dialog_page, focused_id);\n            }\n            Message::DialogUpdate(dialog_page) => {\n                self.dialog_pages.update_front(dialog_page);\n            }\n            Message::DialogUpdateComplete(dialog_page) => {\n                return Task::batch([\n                    self.update(Message::DialogUpdate(dialog_page)),\n                    self.update(Message::DialogComplete),\n                ]);\n            }\n            Message::ExtractHere(entity_opt) => {\n                let paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                if let Some(destination) = paths\n                    .first()\n                    .and_then(|first| first.parent())\n                    .map(Path::to_path_buf)\n                {\n                    return self.operation(Operation::Extract {\n                        paths,\n                        to: destination,\n                        password: None,\n                    });\n                }\n            }\n            Message::ExtractTo(entity_opt) => {\n                let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                return self.extract_to(&selected_paths);\n            }\n            Message::ExtractToResult(result) => {\n                match result {\n                    DialogResult::Cancel => {}\n                    DialogResult::Open(selected_paths) => {\n                        let mut archive_paths = None;\n                        if let Some(file_dialog) = &self.file_dialog_opt\n                            && let Some(window) = self.windows.remove(&file_dialog.window_id())\n                            && let WindowKind::FileDialog(paths) = window.kind\n                        {\n                            archive_paths = paths;\n                        }\n                        if let Some(archive_paths) = archive_paths\n                            && !selected_paths.is_empty()\n                        {\n                            self.file_dialog_opt = None;\n                            return self.operation(Operation::Extract {\n                                paths: archive_paths,\n                                to: selected_paths[0].clone(),\n                                password: None,\n                            });\n                        }\n                    }\n                }\n                self.file_dialog_opt = None;\n            }\n            Message::FileDialogMessage(dialog_message) => {\n                if let Some(dialog) = &mut self.file_dialog_opt {\n                    return dialog.update(dialog_message);\n                }\n            }\n            Message::Key(window_id, modifiers, key, text) => {\n                #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id);\n                #[cfg(not(all(feature = \"wayland\", feature = \"desktop-applet\")))]\n                let in_surface_ids = false;\n                if self.core.main_window_id() == Some(window_id) || in_surface_ids {\n                    let entity = self.tab_model.active();\n                    for (key_bind, action) in &self.key_binds {\n                        if key_bind.matches(modifiers, &key) {\n                            return self.update(action.message(Some(entity)));\n                        }\n                    }\n\n                    // Uncaptured keys with only shift modifiers go to the search or location box\n                    if matches!(self.mode, Mode::App)\n                        && !modifiers.logo()\n                        && !modifiers.control()\n                        && !modifiers.alt()\n                        && matches!(key, Key::Character(_))\n                        && let Some(text) = text\n                    {\n                        match self.config.type_to_search {\n                            TypeToSearch::Recursive => {\n                                let mut term = self.search_get().unwrap_or_default().to_string();\n                                term.push_str(&text);\n                                return self.search_set_active(Some(term));\n                            }\n                            TypeToSearch::EnterPath => {\n                                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                                    let location = tab\n                                        .edit_location\n                                        .as_ref()\n                                        .map_or_else(|| &tab.location, |x| &x.location);\n                                    // Try to add text to end of location\n                                    if let Location::Network(uri, ..) = location {\n                                        let mut uri_string = uri.clone();\n                                        uri_string.push_str(&text);\n                                        tab.edit_location =\n                                            Some(location.with_uri(uri_string).into());\n                                    } else if let Some(path) = location.path_opt() {\n                                        let mut path_string = path.to_string_lossy().into_owned();\n                                        path_string.push_str(&text);\n                                        tab.edit_location =\n                                            Some(location.with_path(path_string.into()).into());\n                                    }\n                                }\n                            }\n                            TypeToSearch::SelectByPrefix => {\n                                // Reset buffer if timeout elapsed\n                                if let Some(last_key) = self.type_select_last_key\n                                    && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT\n                                {\n                                    self.type_select_prefix.clear();\n                                }\n\n                                // Accumulate character and select\n                                self.type_select_prefix.push_str(&text.to_lowercase());\n                                self.type_select_last_key = Some(Instant::now());\n\n                                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                                    tab.select_by_prefix(&self.type_select_prefix);\n                                    if let Some(offset) = tab.select_focus_scroll() {\n                                        return scrollable::scroll_to(\n                                            tab.scrollable_id.clone(),\n                                            AbsoluteOffset {\n                                                x: Some(offset.x),\n                                                y: Some(offset.y),\n                                            },\n                                        );\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            Message::MaybeExit => {\n                if self.core.main_window_id().is_none() && self.pending_operations.is_empty() {\n                    // Exit if window is closed and there are no pending operations\n                    process::exit(0);\n                }\n            }\n            Message::LaunchUrl(url) => match open::that_detached(&url) {\n                Ok(()) => {}\n                Err(err) => {\n                    log::warn!(\"failed to open {url:?}: {err}\");\n                }\n            },\n            Message::ModifiersChanged(window_id, modifiers) => {\n                #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id);\n                #[cfg(not(all(feature = \"wayland\", feature = \"desktop-applet\")))]\n                let in_surface_ids = false;\n                if self.core.main_window_id() == Some(window_id) || in_surface_ids {\n                    self.modifiers = modifiers;\n                }\n                if let Some(window) = self.windows.get_mut(&window_id) {\n                    window.modifiers = modifiers;\n                }\n            }\n            Message::MounterItems(mounter_key, mounter_items) => {\n                // Check for unmounted folders\n                let mut unmounted = Vec::new();\n                if let Some(old_items) = self.mounter_items.get(&mounter_key) {\n                    for old_item in old_items {\n                        if let Some(old_path) = old_item.path()\n                            && old_item.is_mounted()\n                        {\n                            let mut still_mounted = false;\n                            for item in &mounter_items {\n                                if let Some(path) = item.path()\n                                    && path == old_path\n                                    && item.is_mounted()\n                                {\n                                    still_mounted = true;\n                                    break;\n                                }\n                            }\n                            if !still_mounted {\n                                unmounted.push(old_path);\n                            }\n                        }\n                    }\n                }\n\n                // Go back to home in any tabs that were unmounted\n                let mut commands = Vec::new();\n                {\n                    let home_location = Location::Path(home_dir());\n                    let entities: Box<[_]> = self.tab_model.iter().collect();\n                    for entity in entities {\n                        let title_opt = self.tab_model.data_mut::<Tab>(entity).and_then(|tab| {\n                            unmounted\n                                .iter()\n                                .any(|unmounted| {\n                                    tab.location\n                                        .path_opt()\n                                        .is_some_and(|location| location.starts_with(unmounted))\n                                })\n                                .then(|| {\n                                    tab.change_location(&home_location, None);\n                                    tab.title()\n                                })\n                        });\n                        if let Some(title) = title_opt {\n                            self.tab_model.text_set(entity, title);\n                            commands.push(self.update_tab(entity, home_location.clone(), None));\n                        }\n                    }\n                    if !commands.is_empty() {\n                        commands.push(self.update_title());\n                        commands.push(self.update_watcher());\n                    }\n                }\n\n                // Insert new items\n                self.mounter_items.insert(mounter_key, mounter_items);\n\n                // Update nav bar\n                //TODO: this could change favorites IDs while they are in use\n                self.update_nav_model();\n\n                // Update desktop tabs\n                commands.push(self.update_desktop());\n\n                return Task::batch(commands);\n            }\n            Message::MountResult(mounter_key, item, res) => match res {\n                Ok(true) => {\n                    log::info!(\"connected to {item:?}\");\n                    // Automatically navigate to the mounted location\n                    if let Some(path) = item.path() {\n                        let location = if item.is_remote() {\n                            Location::Network(item.uri(), item.name(), Some(path))\n                        } else {\n                            Location::Path(path)\n                        };\n                        let message = Message::TabMessage(None, tab::Message::Location(location));\n                        return self.update(message);\n                    }\n                }\n                Ok(false) => {\n                    log::info!(\"cancelled connection to {item:?}\");\n                }\n                Err(error) => {\n                    log::warn!(\"failed to connect to {item:?}: {error}\");\n                    return self.push_dialog(\n                        DialogPage::MountError {\n                            mounter_key,\n                            item,\n                            error,\n                        },\n                        Some(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()),\n                    );\n                }\n            },\n            Message::Mouse(window_id, _button) => {\n                // Close context menu when clicking outside.\n                if self.core.main_window_id() == Some(window_id) {\n                    return self.close_context_menus();\n                }\n            }\n            Message::MoveTo(entity_opt) => {\n                let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                return self.move_to(&selected_paths);\n            }\n            Message::MoveToResult(result) => {\n                match result {\n                    DialogResult::Cancel => {}\n                    DialogResult::Open(selected_paths) => {\n                        let mut file_paths = None;\n                        if let Some(file_dialog) = &self.file_dialog_opt\n                            && let Some(window) = self.windows.remove(&file_dialog.window_id())\n                            && let WindowKind::FileDialog(paths) = window.kind\n                        {\n                            file_paths = paths;\n                        }\n                        if let Some(file_paths) = file_paths\n                            && !selected_paths.is_empty()\n                        {\n                            self.file_dialog_opt = None;\n                            return self.operation(Operation::Move {\n                                paths: file_paths.to_vec(),\n                                to: selected_paths[0].clone(),\n                                cross_device_copy: false,\n                            });\n                        }\n                    }\n                }\n                self.file_dialog_opt = None;\n            }\n            Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {\n                return self.push_dialog(\n                    DialogPage::NetworkAuth {\n                        mounter_key,\n                        uri,\n                        auth,\n                        auth_tx,\n                    },\n                    Some(self.dialog_text_input.clone()),\n                );\n            }\n            Message::NetworkDriveInput(input) => {\n                self.network_drive_input = input;\n            }\n            Message::NetworkDriveSubmit => {\n                //TODO: know which mounter to use for network drives\n                if let Some((mounter_key, mounter)) = MOUNTERS.iter().next() {\n                    self.network_drive_connecting =\n                        Some((*mounter_key, self.network_drive_input.clone()));\n                    return mounter\n                        .network_drive(self.network_drive_input.clone())\n                        .map(|()| cosmic::action::none());\n                }\n                log::warn!(\n                    \"no mounter found for connecting to {:?}\",\n                    self.network_drive_input\n                );\n            }\n            Message::NetworkResult(mounter_key, uri, res) => {\n                if self\n                    .network_drive_connecting\n                    .as_ref()\n                    .is_some_and(|(m, u)| *m == mounter_key && *u == uri)\n                {\n                    self.network_drive_connecting = None;\n                }\n                match res {\n                    Ok(true) => {\n                        log::info!(\"connected to {uri:?}\");\n                        if matches!(self.context_page, ContextPage::NetworkDrive) {\n                            self.set_show_context(false);\n                        }\n                    }\n                    Ok(false) => {\n                        log::info!(\"cancelled connection to {uri:?}\");\n                    }\n                    Err(error) => {\n                        log::warn!(\"failed to connect to {uri:?}: {error}\");\n                        return self.dialog_pages.push_back(DialogPage::NetworkError {\n                            mounter_key,\n                            uri,\n                            error,\n                        });\n                    }\n                }\n            }\n            Message::NewItem(entity_opt, dir) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                    && let Some(path) = tab.location.path_opt()\n                {\n                    return Task::batch([\n                        self.dialog_pages.push_back(DialogPage::NewItem {\n                            parent: path.clone(),\n                            name: String::new(),\n                            dir,\n                        }),\n                        widget::text_input::focus(self.dialog_text_input.clone()),\n                    ]);\n                }\n            }\n            #[cfg(feature = \"notify\")]\n            Message::Notification(notification) => {\n                self.notification_opt = Some(notification);\n            }\n            Message::NotifyEvents(events) => {\n                log::debug!(\"{events:?}\");\n\n                let mut needs_reload = Vec::new();\n                let entities: Box<[_]> = self.tab_model.iter().collect();\n                for entity in entities {\n                    if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                        && let Some(path) = tab.location.path_opt()\n                    {\n                        let mut contains_change = false;\n                        for event in &events {\n                            for event_path in &event.paths {\n                                if event_path.starts_with(path) {\n                                    if let notify::EventKind::Modify(\n                                        notify::event::ModifyKind::Metadata(_)\n                                        | notify::event::ModifyKind::Data(_),\n                                    ) = event.kind\n                                    {\n                                        // If metadata or data changed, find the matching item and reload it\n                                        //TODO: this could be further optimized by looking at what exactly changed\n                                        if let Some(items) = &mut tab.items_opt {\n                                            for item in items.iter_mut() {\n                                                if item.path_opt() == Some(event_path) {\n                                                    //TODO: reload more, like mime types?\n                                                    match fs::metadata(event_path) {\n                                                        Ok(new_metadata) => {\n                                                            if let ItemMetadata::Path {\n                                                                metadata,\n                                                                ..\n                                                            } = &mut item.metadata\n                                                            {\n                                                                *metadata = new_metadata;\n                                                            }\n                                                        }\n\n                                                        Err(err) => {\n                                                            log::warn!(\n                                                                \"failed to reload metadata for {}: {}\",\n                                                                path.display(),\n                                                                err\n                                                            );\n                                                        }\n                                                    }\n                                                    //TODO item.thumbnail_opt =\n                                                }\n                                            }\n                                        }\n                                    } else {\n                                        // Any other events reload the whole tab\n                                        contains_change = true;\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                        if contains_change {\n                            needs_reload.push((entity, tab.location.clone()));\n                        }\n                    }\n                }\n\n                let commands = needs_reload\n                    .into_iter()\n                    .map(|(entity, location)| self.update_tab(entity, location, None));\n                return Task::batch(commands);\n            }\n            Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()\n            {\n                Some(watcher) => {\n                    self.watcher_opt = Some((watcher, FxHashSet::default()));\n                    return self.update_watcher();\n                }\n                None => {\n                    log::warn!(\"message did not contain notify watcher\");\n                }\n            },\n            Message::OpenTerminal(entity_opt) => {\n                if let Some(terminal) = self.mime_app_cache.terminal() {\n                    let mut paths = Box::from([]);\n                    let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                    if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                        && let Some(path) = tab.location.path_opt()\n                    {\n                        if let Some(items) = tab.items_opt() {\n                            paths = items\n                                .iter()\n                                .filter_map(\n                                    |item| {\n                                        if item.selected { item.path_opt() } else { None }\n                                    },\n                                )\n                                .collect();\n                        }\n                        if paths.is_empty() {\n                            paths = Box::from([path]);\n                        }\n                    }\n                    for path in paths {\n                        if let Some(mut command) = terminal\n                            .command::<&str>(&[])\n                            .and_then(|v| v.into_iter().next())\n                        {\n                            command.current_dir(path);\n                            if let Err(err) = spawn_detached(&mut command) {\n                                log::warn!(\n                                    \"failed to open {} with terminal {:?}: {}\",\n                                    path.display(),\n                                    terminal.id,\n                                    err\n                                );\n                            }\n                        } else {\n                            log::warn!(\"failed to get command for {:?}\", terminal.id);\n                        }\n                    }\n                }\n            }\n            Message::OpenInNewTab(entity_opt) => {\n                let selected_paths: Box<[_]> = self\n                    .selected_paths(entity_opt)\n                    .filter(|p| p.is_dir())\n                    .collect();\n                return Task::batch(\n                    selected_paths\n                        .into_iter()\n                        .map(|path| self.open_tab(Location::Path(path), false, None)),\n                );\n            }\n            Message::OpenInNewWindow(entity_opt) => match env::current_exe() {\n                Ok(exe) => self\n                    .selected_paths(entity_opt)\n                    .filter(|p| p.is_dir())\n                    .for_each(|path| match process::Command::new(&exe).arg(path).spawn() {\n                        Ok(_child) => {}\n                        Err(err) => {\n                            log::error!(\"failed to execute {}: {}\", exe.display(), err);\n                        }\n                    }),\n                Err(err) => {\n                    log::error!(\"failed to get current executable path: {err}\");\n                }\n            },\n            Message::OpenItemLocation(entity_opt) => {\n                let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                return Task::batch(selected_paths.into_iter().filter_map(|path| {\n                    path.parent()\n                        .map(Path::to_path_buf)\n                        .map(|parent| self.open_tab(Location::Path(parent), true, Some(vec![path])))\n                }));\n            }\n            Message::OpenWithBrowse => match self.dialog_pages.pop_front() {\n                Some((\n                    DialogPage::OpenWith {\n                        mime,\n                        store_opt: Some(app),\n                        ..\n                    },\n                    task,\n                )) => {\n                    let url = format!(\"mime:///{mime}\");\n                    // TODO: Support multiple URLs\n                    if let Some(mut command) =\n                        app.command(&[&url]).and_then(|v| v.into_iter().next())\n                    {\n                        if let Err(err) = spawn_detached(&mut command) {\n                            log::warn!(\"failed to open {:?} with {:?}: {}\", url, app.id, err);\n                        }\n                    } else {\n                        log::warn!(\n                            \"failed to open {:?} with {:?}: failed to get command\",\n                            url,\n                            app.id\n                        );\n                    }\n                    return task;\n                }\n                Some((dialog_page, task)) => {\n                    log::warn!(\"tried to open with browse from the wrong dialog\");\n                    return Task::batch([task, self.dialog_pages.push_front(dialog_page)]);\n                }\n                None => {}\n            },\n            Message::OpenWithDialog(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data::<Tab>(entity)\n                    && let Some(items) = tab.items_opt()\n                {\n                    for item in items {\n                        if !item.selected {\n                            continue;\n                        }\n                        let Some(path) = item.path_opt() else {\n                            continue;\n                        };\n                        return self.push_dialog(\n                            DialogPage::OpenWith {\n                                path: path.clone(),\n                                mime: item.mime.clone(),\n                                selected: 0,\n                                store_opt: \"x-scheme-handler/mime\"\n                                    .parse::<mime_guess::Mime>()\n                                    .ok()\n                                    .and_then(|mime| {\n                                        self.mime_app_cache.get(&mime).first().cloned()\n                                    }),\n                            },\n                            Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),\n                        );\n                    }\n                }\n            }\n            Message::OpenWithSelection(index) => {\n                if let Some(DialogPage::OpenWith { selected, .. }) = self.dialog_pages.front_mut() {\n                    *selected = index;\n                }\n            }\n            Message::Paste(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                    && let Some(path) = tab.location.path_opt()\n                {\n                    let to = path.clone();\n\n                    // Use cached clipboard data if available (needed for Wayland popups)\n                    match &self.clipboard_cache {\n                        ClipboardCache::Files(contents) => {\n                            if contents.paths.is_empty() {\n                                return iced::Task::future(tokio::time::sleep(\n                                    std::time::Duration::from_millis(300),\n                                ))\n                                .discard()\n                                .chain(\n                                    clipboard::read_data::<ClipboardPaste>().map(\n                                        move |contents_opt| match contents_opt {\n                                            Some(contents) => cosmic::action::app(\n                                                Message::PasteContents(to.clone(), contents),\n                                            ),\n                                            None => {\n                                                cosmic::action::app(Message::PasteImage(to.clone()))\n                                            }\n                                        },\n                                    ),\n                                );\n                            }\n                            return self\n                                .update(Message::PasteContents(to.clone(), contents.clone()));\n                        }\n                        ClipboardCache::Image(contents) => {\n                            return self\n                                .update(Message::PasteImageContents(to.clone(), contents.clone()));\n                        }\n                        ClipboardCache::Video(contents) => {\n                            return self\n                                .update(Message::PasteVideoContents(to.clone(), contents.clone()));\n                        }\n                        ClipboardCache::Text(contents) => {\n                            return self\n                                .update(Message::PasteTextContents(to.clone(), contents.clone()));\n                        }\n                        ClipboardCache::Empty => {\n                            // Cache is empty, try reading from clipboard directly\n                            // (works when triggered from main window, e.g., Ctrl+V)\n                            return clipboard::read_data::<ClipboardPaste>().map(\n                                move |contents_opt| match contents_opt {\n                                    Some(contents) => cosmic::action::app(Message::PasteContents(\n                                        to.clone(),\n                                        contents,\n                                    )),\n                                    None => cosmic::action::app(Message::PasteImage(to.clone())),\n                                },\n                            );\n                        }\n                    }\n                }\n            }\n            Message::PasteContents(to, mut contents) => {\n                contents.paths.retain(|p| *p != to);\n                if !contents.paths.is_empty() {\n                    return match contents.kind {\n                        ClipboardKind::Copy => self.operation(Operation::Copy {\n                            paths: contents.paths,\n                            to,\n                        }),\n                        ClipboardKind::Cut { is_dnd } => self.operation(Operation::Move {\n                            paths: contents.paths,\n                            to,\n                            cross_device_copy: is_dnd,\n                        }),\n                    };\n                }\n            }\n            Message::PasteImage(to) => {\n                return clipboard::read_data::<ClipboardPasteImage>().map(move |contents_opt| {\n                    match contents_opt {\n                        Some(contents) => {\n                            cosmic::action::app(Message::PasteImageContents(to.clone(), contents))\n                        }\n                        // No image data in clipboard, try video data\n                        None => cosmic::action::app(Message::PasteVideo(to.clone())),\n                    }\n                });\n            }\n            Message::PasteImageContents(to, contents) => {\n                let Some(extension) = contents.extension() else {\n                    log::warn!(\n                        \"Ignoring paste: unknown image MIME type {:?}\",\n                        contents.mime_type\n                    );\n                    return Task::none();\n                };\n\n                // Generate unique filename for the pasted image\n                let base_name = format!(\"{}.{}\", fl!(\"pasted-image\"), extension);\n                let base_path = to.join(&base_name);\n                let final_path = copy_unique_path(&base_path, &to);\n\n                // Write image data to file\n                match fs::write(&final_path, &contents.data) {\n                    Ok(_) => {\n                        log::info!(\"Pasted image saved to {:?}\", final_path);\n                    }\n                    Err(err) => {\n                        log::error!(\"Failed to save pasted image: {}\", err);\n                    }\n                }\n            }\n            Message::PasteVideo(to) => {\n                return clipboard::read_data::<ClipboardPasteVideo>().map(move |contents_opt| {\n                    match contents_opt {\n                        Some(contents) => {\n                            cosmic::action::app(Message::PasteVideoContents(to.clone(), contents))\n                        }\n                        // No video data in clipboard, try text data\n                        None => cosmic::action::app(Message::PasteText(to.clone())),\n                    }\n                });\n            }\n            Message::PasteVideoContents(to, contents) => {\n                let Some(extension) = contents.extension() else {\n                    log::warn!(\n                        \"Ignoring paste: unknown video MIME type {:?}\",\n                        contents.mime_type\n                    );\n                    return Task::none();\n                };\n\n                // Generate unique filename for the pasted video\n                let base_name = format!(\"{}.{}\", fl!(\"pasted-video\"), extension);\n                let base_path = to.join(&base_name);\n                let final_path = copy_unique_path(&base_path, &to);\n\n                // Write video data to file\n                match fs::write(&final_path, &contents.data) {\n                    Ok(_) => {\n                        log::info!(\"Pasted video saved to {:?}\", final_path);\n                    }\n                    Err(err) => {\n                        log::error!(\"Failed to save pasted video: {}\", err);\n                    }\n                }\n            }\n            Message::PasteText(to) => {\n                return clipboard::read_data::<ClipboardPasteText>().map(move |contents_opt| {\n                    match contents_opt {\n                        Some(contents) => {\n                            cosmic::action::app(Message::PasteTextContents(to.clone(), contents))\n                        }\n                        None => cosmic::action::none(),\n                    }\n                });\n            }\n            Message::PasteTextContents(to, contents) => {\n                // Generate unique filename for the pasted text\n                let base_name = format!(\"{}.txt\", fl!(\"pasted-text\"));\n                let base_path = to.join(&base_name);\n                let final_path = copy_unique_path(&base_path, &to);\n\n                // Write text data to file\n                match fs::write(&final_path, &contents.data) {\n                    Ok(_) => {\n                        log::info!(\"Pasted text saved to {:?}\", final_path);\n                    }\n                    Err(err) => {\n                        log::error!(\"Failed to save pasted text: {}\", err);\n                    }\n                }\n            }\n            Message::CheckClipboard => {\n                // Check if clipboard has any paste-able content and cache it\n                return clipboard::read_data::<ClipboardPaste>().map(|contents_opt| {\n                    match contents_opt {\n                        Some(contents) if contents.paths.is_empty() => cosmic::action::app(\n                            Message::RetryCheckClipboard(ClipboardCache::Files(contents)),\n                        ),\n                        Some(contents) => cosmic::action::app(Message::ClipboardCached(\n                            ClipboardCache::Files(contents),\n                        )),\n                        _ => cosmic::action::app(Message::CheckClipboardImage),\n                    }\n                });\n            }\n            Message::CheckClipboardImage => {\n                return clipboard::read_data::<ClipboardPasteImage>().map(|contents_opt| {\n                    match contents_opt {\n                        Some(contents) => cosmic::action::app(Message::ClipboardCached(\n                            ClipboardCache::Image(contents),\n                        )),\n                        None => cosmic::action::app(Message::CheckClipboardVideo),\n                    }\n                });\n            }\n            Message::CheckClipboardVideo => {\n                return clipboard::read_data::<ClipboardPasteVideo>().map(|contents_opt| {\n                    match contents_opt {\n                        Some(contents) => cosmic::action::app(Message::ClipboardCached(\n                            ClipboardCache::Video(contents),\n                        )),\n                        None => cosmic::action::app(Message::CheckClipboardText),\n                    }\n                });\n            }\n            Message::CheckClipboardText => {\n                return clipboard::read_data::<ClipboardPasteText>().map(|contents_opt| {\n                    cosmic::action::app(Message::ClipboardCached(match contents_opt {\n                        Some(contents) => ClipboardCache::Text(contents),\n                        None => ClipboardCache::Empty,\n                    }))\n                });\n            }\n            Message::RetryCheckClipboard(cache) => {\n                let mut cmds = Vec::new();\n                cmds.push(self.update(Message::ClipboardCached(cache)));\n\n                cmds.push(\n                    iced::Task::future(tokio::time::sleep(Duration::from_millis(300)))\n                        .discard()\n                        .chain(\n                            clipboard::read_data::<ClipboardPaste>().map(|contents_opt| {\n                                match contents_opt {\n                                    Some(contents) if !contents.paths.is_empty() => {\n                                        cosmic::action::app(Message::ClipboardCached(\n                                            ClipboardCache::Files(contents),\n                                        ))\n                                    }\n                                    _ => cosmic::action::app(Message::CheckClipboardImage),\n                                }\n                            }),\n                        ),\n                );\n                return Task::batch(cmds);\n            }\n            Message::ClipboardCached(cache) => {\n                self.clipboard_cache = cache;\n            }\n            Message::PendingCancel(id) => {\n                if let Some((_, controller)) = self.pending_operations.get(&id) {\n                    controller.cancel();\n                    self.progress_operations.remove(&id);\n                }\n            }\n            Message::PendingCancelAll => {\n                for (id, (_, controller)) in &self.pending_operations {\n                    controller.cancel();\n                    self.progress_operations.remove(id);\n                }\n            }\n            Message::PendingComplete(id, op_sel) => {\n                return self.handle_completed_operations(vec![(id, op_sel)]);\n            }\n            Message::PendingDismiss => {\n                self.progress_operations.clear();\n            }\n            Message::PendingError(id, err) => {\n                return self.handle_operation_errors(vec![(id, err)]);\n            }\n            Message::PendingResults(completed, errors) => {\n                return Task::batch(vec![\n                    self.handle_completed_operations(completed),\n                    self.handle_operation_errors(errors),\n                ]);\n            }\n            Message::PendingPause(id, pause) => {\n                if let Some((_, controller)) = self.pending_operations.get(&id) {\n                    if pause {\n                        controller.pause();\n                    } else {\n                        controller.unpause();\n                    }\n                }\n            }\n            Message::PendingPauseAll(pause) => {\n                for (_, controller) in self.pending_operations.values() {\n                    if pause {\n                        controller.pause();\n                    } else {\n                        controller.unpause();\n                    }\n                }\n            }\n            Message::PermanentlyDelete(entity_opt) => {\n                let paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                if !paths.is_empty() {\n                    return self.push_dialog(\n                        DialogPage::PermanentlyDelete { paths },\n                        Some(PERMANENT_DELETE_BUTTON_ID.clone()),\n                    );\n                }\n            }\n            Message::Preview(entity_opt) => {\n                match self.mode {\n                    Mode::App => {\n                        let show_details = !self.config.show_details;\n                        self.context_page = ContextPage::Preview(None, PreviewKind::Selected);\n                        self.core.window.show_context = show_details;\n                        return cosmic::task::message(Message::SetShowDetails(show_details));\n                    }\n                    Mode::Desktop => {\n                        let preview_kind = {\n                            let mut selected_paths = self.selected_paths(entity_opt);\n                            match (selected_paths.next(), selected_paths.next()) {\n                                (Some(_), Some(_)) => Some(PreviewKind::Selected),\n                                (Some(path), None) => {\n                                    Some(PreviewKind::Location(Location::Path(path)))\n                                }\n                                _ => None,\n                            }\n                        };\n\n                        if let Some(preview_kind) = preview_kind {\n                            let mut settings = window::Settings {\n                                decorations: true,\n                                min_size: Some(Size::new(360.0, 180.0)),\n                                resizable: true,\n                                size: Size::new(480.0, 600.0),\n                                transparent: true,\n                                ..Default::default()\n                            };\n\n                            #[cfg(target_os = \"linux\")]\n                            {\n                                // Use the dialog ID to make it float\n                                settings.platform_specific.application_id =\n                                    \"com.system76.CosmicFilesDialog\".to_string();\n                            }\n\n                            let (id, command) = window::open(settings);\n                            self.windows.insert(\n                                id,\n                                Window::new(WindowKind::Preview(entity_opt, preview_kind)),\n                            );\n                            return Task::batch([\n                                self.update_desktop(), // Force re-calculating of directory sizes\n                                command.map(|_id| cosmic::action::none()),\n                            ]);\n                        }\n                    }\n                }\n            }\n            Message::RemoveFromRecents(entity_opt) => {\n                let paths: Box<[_]> = self.selected_paths(entity_opt).collect();\n                return self.operation(Operation::RemoveFromRecents { paths });\n            }\n            Message::RescanRecents => {\n                return self.rescan_recents();\n            }\n            Message::RescanTrash => {\n                // Update trash icon if empty/full\n                let maybe_entity = self.nav_model.iter().find(|&entity| {\n                    self.nav_model\n                        .data::<Location>(entity)\n                        .is_some_and(|loc| matches!(loc, Location::Trash))\n                });\n                if let Some(entity) = maybe_entity {\n                    self.nav_model\n                        .icon_set(entity, icon::icon(Trash::icon_symbolic(16)));\n                }\n\n                return Task::batch([self.rescan_trash(), self.update_desktop()]);\n            }\n            Message::Rename(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                    && let Some(items) = tab.items_opt()\n                {\n                    let selected: Box<[_]> = items\n                        .iter()\n                        .filter_map(|item| {\n                            if item.selected {\n                                item.path_opt().cloned()\n                            } else {\n                                None\n                            }\n                        })\n                        .collect();\n                    if !selected.is_empty() {\n                        //TODO: batch rename\n                        let mut last_name = String::new();\n                        let tasks: Vec<_> = selected\n                            .into_iter()\n                            .filter_map(|path| {\n                                let parent = path.parent()?.to_path_buf();\n                                let name = path.file_name()?.to_str()?.to_string();\n                                let dir = path.is_dir();\n                                last_name = name.clone();\n                                Some(self.dialog_pages.push_back(DialogPage::RenameItem {\n                                    from: path,\n                                    parent,\n                                    name,\n                                    dir,\n                                }))\n                            })\n                            .collect();\n                        let tasks = tasks.into_iter().chain([\n                            widget::text_input::focus(self.dialog_text_input.clone()),\n                            widget::text_input::select_until_last(\n                                self.dialog_text_input.clone(),\n                                &last_name,\n                                '.',\n                            ),\n                        ]);\n                        return Task::batch(tasks);\n                    }\n                }\n            }\n            Message::ReplaceResult(replace_result) => {\n                if let Some((dialog_page, task)) = self.dialog_pages.pop_front() {\n                    match dialog_page {\n                        DialogPage::Replace { tx, .. } => {\n                            return Task::future(async move {\n                                let _ = tx.send(replace_result).await;\n                                cosmic::action::none()\n                            });\n                        }\n                        other => {\n                            log::warn!(\"tried to send replace result to the wrong dialog\");\n                            return Task::batch([task, self.dialog_pages.push_front(other)]);\n                        }\n                    }\n                }\n            }\n            Message::RestoreFromTrash(entity_opt) => {\n                let mut trash_items = Vec::new();\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)\n                    && let Some(items) = tab.items_opt()\n                {\n                    for item in items {\n                        if item.selected {\n                            if let ItemMetadata::Trash { entry, .. } = &item.metadata {\n                                trash_items.push(entry.clone());\n                            } else {\n                                //TODO: error on trying to restore non-trash file?\n                            }\n                        }\n                    }\n                }\n                if !trash_items.is_empty() {\n                    return self.operation(Operation::Restore { items: trash_items });\n                }\n            }\n            Message::ScrollTab(scroll_speed) => {\n                let entity = self.tab_model.active();\n                return self.update(Message::TabMessage(\n                    Some(entity),\n                    tab::Message::ScrollTab(f32::from(scroll_speed) / 10.0),\n                ));\n            }\n            Message::SearchActivate => {\n                let mut tasks = vec![self.close_context_menus()];\n\n                if self.search_get().is_none() {\n                    tasks.push(self.search_set_active(Some(String::new())));\n                } else {\n                    tasks.push(widget::text_input::focus(self.search_id.clone()));\n                };\n\n                return Task::batch(tasks);\n            }\n            Message::SearchClear => {\n                return Task::batch([self.close_context_menus(), self.search_set_active(None)]);\n            }\n            Message::SearchInput(input) => {\n                return self.search_set_active(Some(input));\n            }\n            Message::SetShowDetails(show_details) => {\n                config_set!(show_details, show_details);\n                return self.update_config();\n            }\n            Message::SetShowRecents(show_recents) => {\n                config_set!(show_recents, show_recents);\n                return self.update_config();\n            }\n            Message::SetTypeToSearch(type_to_search) => {\n                config_set!(type_to_search, type_to_search);\n                return self.update_config();\n            }\n            Message::SystemThemeModeChange => {\n                return self.update_config();\n            }\n            Message::TabActivate(entity) => {\n                let mut tasks = vec![self.close_context_menus()];\n\n                // Activate new tab\n                self.tab_model.activate(entity);\n                if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n                    {\n                        //Restore scroll\n                        //TODO: why do scrollers with different IDs get the same scroll position?\n                        let scroll = tab.scroll_opt.unwrap_or_default();\n                        tasks.push(scrollable::scroll_to(\n                            tab.scrollable_id.clone(),\n                            AbsoluteOffset {\n                                x: Some(scroll.x),\n                                y: Some(scroll.y),\n                            },\n                        ));\n                    }\n                    self.activate_nav_model_location(&tab.location.clone());\n                }\n                tasks.push(self.update_title());\n                return Task::batch(tasks);\n            }\n            Message::TabNext => {\n                let len = self.tab_model.len();\n                let pos = (self\n                    .tab_model\n                    .position(self.tab_model.active())\n                    .expect(\"should always be at least one tab open\")\n                    + 1)\n                    // Wraparound to 0 if i + 1 > num of tabs\n                    % len as u16;\n\n                let entity = self.tab_model.entity_at(pos);\n                if let Some(entity) = entity {\n                    return self.update(Message::TabActivate(entity));\n                }\n            }\n            Message::TabPrev => {\n                let pos = self\n                    .tab_model\n                    .position(self.tab_model.active())\n                    .expect(\"should always be at least one tab open\")\n                    .checked_sub(1)\n                    // Subtraction underflow => last tab; i.e. it wraps around\n                    .unwrap_or_else(|| (self.tab_model.len() as u16).saturating_sub(1));\n\n                let entity = self.tab_model.entity_at(pos);\n                if let Some(entity) = entity {\n                    return self.update(Message::TabActivate(entity));\n                }\n            }\n            Message::TabClose(entity_opt) => {\n                let mut tasks = Vec::with_capacity(2);\n\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n\n                // If the last tab is closed, close the window\n                // Otherwise, activate closest item\n                if self.tab_model.len() == 1 {\n                    tasks.push(Task::future(async move {\n                        cosmic::action::app(Message::WindowClose)\n                    }));\n                } else if let Some(position) = self.tab_model.position(entity) {\n                    let new_position = if position > 0 {\n                        position - 1\n                    } else {\n                        position + 1\n                    };\n\n                    if let Some(new_entity) = self.tab_model.entity_at(new_position) {\n                        tasks.push(self.update(Message::TabActivate(new_entity)));\n                    }\n                }\n\n                // Remove item\n                self.tab_model.remove(entity);\n\n                tasks.push(self.update_watcher());\n\n                return Task::batch(tasks);\n            }\n            Message::TabConfig(config) => {\n                if config != self.config.tab {\n                    config_set!(tab, config);\n                    return self.update_config();\n                }\n            }\n            Message::ToggleFoldersFirst => {\n                let mut config = self.config.tab;\n                config.folders_first = !config.folders_first;\n                return self.update(Message::TabConfig(config));\n            }\n            Message::ToggleShowHidden => {\n                let mut config = self.config.tab;\n                config.show_hidden = !config.show_hidden;\n                return self.update(Message::TabConfig(config));\n            }\n            Message::TabMessage(entity_opt, tab_message) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n\n                let tab_commands = match self.tab_model.data_mut::<Tab>(entity) {\n                    Some(tab) => tab.update(tab_message, self.modifiers),\n                    _ => Vec::new(),\n                };\n\n                let mut commands = Vec::new();\n                for tab_command in tab_commands {\n                    match tab_command {\n                        tab::Command::Action(action) => {\n                            commands.push(self.update(action.message(Some(entity))));\n                        }\n                        tab::Command::AddNetworkDrive => {\n                            self.context_page = ContextPage::NetworkDrive;\n                            self.set_show_context(true);\n                        }\n                        tab::Command::AddToSidebar(path) => {\n                            let mut favorites = self.config.favorites.clone();\n                            let favorite = Favorite::from_path(path);\n                            if !favorites.contains(&favorite) {\n                                favorites.push(favorite);\n                            }\n                            config_set!(favorites, favorites);\n                            commands.push(self.update_config());\n                        }\n                        tab::Command::AutoScroll(scroll_speed) => {\n                            // converting an f32 to an i16 here by multiplying by 10 and casting to i16\n                            // further resolution isn't necessary\n                            if let Some(scroll_speed_float) = scroll_speed {\n                                self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16);\n                            } else {\n                                self.auto_scroll_speed = None;\n                            }\n                        }\n                        tab::Command::ChangeLocation(tab_title, tab_path, selection_paths) => {\n                            self.activate_nav_model_location(&tab_path);\n\n                            self.tab_model.text_set(entity, tab_title);\n                            // clear the prefix selection buffer when changing location\n                            self.type_select_prefix.clear();\n                            commands.push(Task::batch([\n                                self.update_title(),\n                                self.update_watcher(),\n                                self.update_tab(entity, tab_path, selection_paths),\n                            ]));\n                        }\n                        tab::Command::ContextMenu(point_opt, parent_id) => {\n                            #[cfg(feature = \"wayland\")]\n                            if let Some(point) = point_opt {\n                                if crate::is_wayland() {\n                                    // Open context menu\n                                    use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{\n                                        Anchor, Gravity,\n                                    };\n                                    use cosmic::iced::runtime::platform_specific::wayland::popup::{\n                                        SctkPopupSettings, SctkPositioner,\n                                    };\n                                    let window_id = WindowId::unique();\n                                    self.windows.insert(\n                                        window_id,\n                                        Window::new(WindowKind::ContextMenu(\n                                            entity,\n                                            widget::Id::unique(),\n                                        )),\n                                    );\n                                    commands.push(self.update(Message::CheckClipboard));\n                                    commands.push(self.update(Message::Surface(\n                                        cosmic::surface::action::app_popup(\n                                            move |app: &mut Self| -> SctkPopupSettings {\n                                                let anchor_rect = Rectangle {\n                                                    x: point.x as i32,\n                                                    y: point.y as i32,\n                                                    width: 1,\n                                                    height: 1,\n                                                };\n                                                let positioner = SctkPositioner {\n                                                    size: None,\n                                                    anchor_rect,\n                                                    anchor: Anchor::None,\n                                                    gravity: Gravity::BottomRight,\n                                                    reactive: true,\n                                                    ..Default::default()\n                                                };\n                                                SctkPopupSettings {\n                                                    parent: parent_id.unwrap_or(\n                                                        app.core\n                                                            .main_window_id()\n                                                            .unwrap_or(WindowId::NONE),\n                                                    ),\n                                                    id: window_id,\n                                                    positioner,\n                                                    parent_size: None,\n                                                    grab: true,\n                                                    close_with_children: false,\n                                                    input_zone: None,\n                                                }\n                                            },\n                                            None,\n                                        ),\n                                    )));\n                                }\n                            } else {\n                                // Destroy previous popup\n                                let mut window_ids = Vec::new();\n                                for (window_id, window) in &self.windows {\n                                    if let WindowKind::ContextMenu(e, _) = &window.kind\n                                        && *e == entity\n                                    {\n                                        window_ids.push(*window_id);\n                                    }\n                                }\n                                for window_id in window_ids {\n                                    commands.push(self.update(Message::Surface(\n                                        cosmic::surface::action::destroy_popup(window_id),\n                                    )));\n                                }\n                            }\n                        }\n                        tab::Command::Delete(paths) => commands.push(self.delete(paths)),\n                        tab::Command::DropFiles(to, from) => {\n                            commands.push(self.update(Message::PasteContents(to, from)));\n                        }\n                        tab::Command::EmptyTrash => {\n                            return self.push_dialog(\n                                DialogPage::EmptyTrash,\n                                Some(EMPTY_TRASH_BUTTON_ID.clone()),\n                            );\n                        }\n                        #[cfg(feature = \"desktop\")]\n                        tab::Command::ExecEntryAction(entry, action) => {\n                            Self::exec_entry_action(&entry, action);\n                        }\n                        tab::Command::RunContextAction(action) => {\n                            let paths: Box<[_]> = self.selected_paths(Some(entity)).collect();\n                            if let Some(preset) = self.config.context_actions.get(action) {\n                                if preset.confirm {\n                                    commands.push(self.push_dialog(\n                                        DialogPage::RunContextAction { action, paths },\n                                        Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),\n                                    ));\n                                } else {\n                                    context_action::run(\n                                        &self.config.context_actions,\n                                        action,\n                                        &paths,\n                                    );\n                                }\n                            } else {\n                                log::warn!(\"invalid context action index `{action}`\");\n                            }\n                        }\n                        tab::Command::Iced(iced_command) => {\n                            commands.push(iced_command.0.map(move |x| {\n                                cosmic::action::app(Message::TabMessage(Some(entity), x))\n                            }));\n                        }\n                        tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)),\n                        tab::Command::OpenInNewTab(path) => {\n                            commands.push(self.close_context_menus());\n                            commands.push(self.open_tab(Location::Path(path), false, None));\n                        }\n                        tab::Command::OpenInNewWindow(path) => match env::current_exe() {\n                            Ok(exe) => match process::Command::new(&exe).arg(path).spawn() {\n                                Ok(_child) => {}\n                                Err(err) => {\n                                    log::error!(\"failed to execute {}: {}\", exe.display(), err);\n                                }\n                            },\n                            Err(err) => {\n                                log::error!(\"failed to get current executable path: {err}\");\n                            }\n                        },\n                        tab::Command::OpenTrash => {\n                            //TODO: use handler for x-scheme-handler/trash and open trash:///\n                            let mut command = process::Command::new(\"cosmic-files\");\n                            command.arg(\"--trash\");\n                            match spawn_detached(&mut command) {\n                                Ok(()) => {}\n                                Err(err) => {\n                                    log::warn!(\"failed to run cosmic-files --trash: {err}\");\n                                }\n                            }\n                        }\n                        tab::Command::Preview(kind) => {\n                            self.context_page = ContextPage::Preview(Some(entity), kind);\n                            self.set_show_context(true);\n                        }\n                        tab::Command::SetOpenWith(mime, id) => {\n                            //TODO: this will block for a few ms, run in background?\n                            self.mime_app_cache.set_default(mime, id);\n                        }\n                        tab::Command::SetPermissions(path, mode) => {\n                            commands.push(self.operation(Operation::SetPermissions { path, mode }));\n                        }\n                        tab::Command::SetMultiplePermissions(permissions) => {\n                            commands.push(\n                                self.join_operations(\n                                    permissions\n                                        .into_iter()\n                                        .map(|(path, mode)| Operation::SetPermissions {\n                                            path,\n                                            mode,\n                                        })\n                                        .collect(),\n                                ),\n                            );\n                        }\n                        tab::Command::WindowDrag => {\n                            if let Some(window_id) = self.core.main_window_id() {\n                                commands.push(window::drag(window_id));\n                            }\n                        }\n                        tab::Command::WindowToggleMaximize => {\n                            if let Some(window_id) = self.core.main_window_id() {\n                                commands.push(window::toggle_maximize(window_id));\n                            }\n                        }\n                        tab::Command::SetSort(location, heading_options, direction) => {\n                            let default_sort = tab::SORT_OPTION_FALLBACK\n                                .get(&location)\n                                .copied()\n                                .unwrap_or((HeadingOptions::Name, true));\n                            let changed = if default_sort == (heading_options, direction) {\n                                self.state.sort_names.remove(&location).is_some()\n                            } else {\n                                // force reordering of inserted values so new settings are not dropped in the truncation step\n                                _ = self.state.sort_names.remove(&location);\n                                _ = self\n                                    .state\n                                    .sort_names\n                                    .insert(location, (heading_options, direction))\n                                    .is_none_or(|old| old != (heading_options, direction));\n\n                                const MAX_SORT_NAMES: usize = 999;\n                                // TODO potentially configurable limit on max size?\n                                if self.state.sort_names.len() > MAX_SORT_NAMES {\n                                    // truncate is not a good fit because it drops the items at the end, which are newest...\n                                    self.state.sort_names = self\n                                        .state\n                                        .sort_names\n                                        .split_off(self.state.sort_names.len() - MAX_SORT_NAMES);\n                                }\n\n                                true\n                            };\n\n                            if !self.must_save_sort_names & changed {\n                                self.must_save_sort_names = true;\n                                return cosmic::Task::future(async move {\n                                    tokio::time::sleep(Duration::from_secs(1)).await;\n                                    cosmic::action::app(Message::SaveSortNames)\n                                });\n                            }\n                        }\n                    }\n                }\n                return Task::batch(commands);\n            }\n            Message::TabNew => {\n                let active = self.tab_model.active();\n                let location = match self.tab_model.data::<Tab>(active) {\n                    Some(tab) => tab.location.clone(),\n                    None => Location::Path(home_dir()),\n                };\n                return self.open_tab(location, true, None);\n            }\n            Message::TabRescan(entity, mut location, parent_item_opt, items, selection_paths) => {\n                location = location.normalize();\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                    tab.location = tab.location.normalize();\n                    if location == tab.location {\n                        tab.parent_item_opt = parent_item_opt;\n                        tab.set_items(items);\n                        let location_str = location.to_string();\n                        let sort = self\n                            .state\n                            .sort_names\n                            .get(&location_str)\n                            .or_else(|| SORT_OPTION_FALLBACK.get(&location_str))\n                            .unwrap_or(&(HeadingOptions::Name, true));\n\n                        tab.sort_name = sort.0;\n                        tab.sort_direction = sort.1;\n\n                        let mut tasks = Vec::with_capacity(2);\n\n                        if let Some(selection_paths) = selection_paths {\n                            tab.select_paths(selection_paths);\n\n                            // Ensure selected path is scrolled to after redraw\n                            tasks.push(Task::done(cosmic::action::app(Message::TabMessage(\n                                Some(entity),\n                                tab::Message::ScrollToFocused,\n                            ))));\n                        }\n\n                        tasks.push(clipboard::read_data::<ClipboardPaste>().map(|p| {\n                            cosmic::action::app(Message::CutPaths(match p {\n                                Some(s) => match s.kind {\n                                    ClipboardKind::Copy => Vec::new(),\n                                    ClipboardKind::Cut { .. } => s.paths,\n                                },\n                                None => Vec::new(),\n                            }))\n                        }));\n\n                        return Task::batch(tasks);\n                    }\n                }\n            }\n            Message::TabView(entity_opt, view) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {\n                    if matches!(tab.mode, tab::Mode::Desktop) {\n                        return Task::none();\n                    }\n\n                    tab.config.view = view;\n                }\n                let mut config = self.config.tab;\n                config.view = view;\n                return self.update(Message::TabConfig(config));\n            }\n            Message::CutPaths(paths) => {\n                if let Some(tab) = self.tab_model.active_data_mut::<Tab>() {\n                    tab.refresh_cut(&paths);\n                }\n            }\n            Message::TimeConfigChange(time_config) => {\n                self.config.tab.military_time = time_config.military_time;\n                return self.update_config();\n            }\n            Message::ToggleContextPage(context_page) => {\n                //TODO: ensure context menus are closed\n                if self.context_page == context_page\n                    || matches!(self.context_page, ContextPage::Preview(_, _))\n                {\n                    self.set_show_context(!self.core.window.show_context);\n                } else {\n                    self.set_show_context(true);\n                }\n                self.context_page = context_page;\n                // Preview status is preserved across restarts\n                if matches!(self.context_page, ContextPage::Preview(_, _)) {\n                    return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(\n                        self.core.window.show_context,\n                    )));\n                }\n            }\n            Message::Undo(_id) => {\n                // TODO: undo\n            }\n            Message::UndoTrash(id, recently_trashed) => {\n                self.toasts.remove(id);\n\n                let mut paths = Vec::with_capacity(recently_trashed.len());\n                let icon_sizes = self.config.tab.icon_sizes;\n\n                return cosmic::task::future(async move {\n                    match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes))\n                        .await\n                    {\n                        Ok((_parent_item_opt, items)) => {\n                            for path in &*recently_trashed {\n                                for item in &items {\n                                    if let ItemMetadata::Trash { ref entry, .. } = item.metadata {\n                                        let original_path = entry.original_path();\n                                        if &original_path == path {\n                                            paths.push(entry.clone());\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        Err(err) => {\n                            log::warn!(\"failed to rescan: {err}\");\n                        }\n                    }\n\n                    Message::UndoTrashStart(paths)\n                });\n            }\n            Message::UndoTrashStart(items) => {\n                return self.operation(Operation::Restore { items });\n            }\n            Message::WindowClose => {\n                if let Some(window_id) = self.core.main_window_id() {\n                    self.core.set_main_window_id(None);\n                    return Task::batch([\n                        window::close(window_id),\n                        Task::future(async move { cosmic::action::app(Message::MaybeExit) }),\n                    ]);\n                }\n            }\n            Message::WindowCloseRequested(id) => {\n                self.remove_window(&id);\n            }\n            Message::WindowMaximize(id, maximized) => {\n                return window::maximize(id, maximized);\n            }\n            Message::WindowNew => match env::current_exe() {\n                Ok(exe) => {\n                    // initialize command to spawn another instance of this application\n                    let mut command = process::Command::new(&exe);\n\n                    // make the new window open at the same location as the currently active tab by\n                    // passing respective command line arguments\n                    let entity = self.tab_model.active();\n                    let active_tab_location =\n                        self.tab_model.data::<Tab>(entity).map(|tab| &tab.location);\n                    match active_tab_location {\n                        Some(\n                            Location::Desktop(path, ..)\n                            | Location::Path(path)\n                            | Location::Search(SearchLocation::Path(path), ..),\n                        ) => {\n                            command.arg(path);\n                        }\n                        Some(Location::Network(uri, ..)) => {\n                            command.arg(uri);\n                        }\n                        Some(Location::Recents | Location::Search(SearchLocation::Recents, ..)) => {\n                            command.arg(\"--recents\");\n                        }\n                        Some(Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {\n                            command.arg(\"--trash\");\n                        }\n                        None => {}\n                    };\n\n                    // spawn the new window\n                    match command.spawn() {\n                        Ok(_child) => {}\n                        Err(err) => {\n                            log::error!(\"failed to execute {}: {}\", exe.display(), err);\n                        }\n                    }\n                }\n                Err(err) => {\n                    log::error!(\"failed to get current executable path: {err}\");\n                }\n            },\n            Message::ZoomDefault(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                let mut config = self.config.tab;\n                if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n                    zoom_to_default(tab.config.view, &mut config.icon_sizes);\n                }\n                return self.update(Message::TabConfig(config));\n            }\n            Message::ZoomIn(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                let mut config = self.config.tab;\n                if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n                    zoom_in_view(tab.config.view, &mut config.icon_sizes);\n                }\n                return self.update(Message::TabConfig(config));\n            }\n            Message::ZoomOut(entity_opt) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                let mut config = self.config.tab;\n                if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n                    zoom_out_view(tab.config.view, &mut config.icon_sizes);\n                }\n                return self.update(Message::TabConfig(config));\n            }\n            Message::DndEnterNav(entity) => {\n                if let Some(location) = self.nav_model.data::<Location>(entity) {\n                    self.nav_dnd_hover = Some((location.clone(), Instant::now()));\n                    let location = location.clone();\n                    return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| {\n                        cosmic::Action::App(Message::DndHoverLocTimeout(location.clone()))\n                    });\n                }\n            }\n            Message::DndExitNav => {\n                self.nav_dnd_hover = None;\n            }\n            Message::DndDropNav(entity, data, action) => {\n                self.nav_dnd_hover = None;\n                if let Some((location, data)) = self.nav_model.data::<Location>(entity).zip(data) {\n                    let kind = match action {\n                        DndAction::Move => ClipboardKind::Cut { is_dnd: true },\n                        _ => ClipboardKind::Copy,\n                    };\n                    let ret = match location {\n                        Location::Path(p) => self.update(Message::PasteContents(\n                            p.clone(),\n                            ClipboardPaste {\n                                kind,\n                                paths: data.paths,\n                            },\n                        )),\n                        Location::Trash if matches!(action, DndAction::Move) => {\n                            self.delete(data.paths)\n                        }\n                        _ => {\n                            log::warn!(\"Copy to trash is not supported.\");\n                            Task::none()\n                        }\n                    };\n                    return ret;\n                }\n            }\n            Message::DndHoverLocTimeout(location) => {\n                if self\n                    .nav_dnd_hover\n                    .as_ref()\n                    .is_some_and(|(loc, i)| *loc == location && i.elapsed() >= HOVER_DURATION)\n                {\n                    self.nav_dnd_hover = None;\n                    let entity = self.tab_model.active();\n                    let title_opt = match self.tab_model.data_mut::<Tab>(entity) {\n                        Some(tab) => {\n                            tab.change_location(&location, None);\n                            Some(tab.title())\n                        }\n                        None => None,\n                    };\n                    if let Some(title) = title_opt {\n                        self.tab_model.text_set(entity, title);\n                        return Task::batch([\n                            self.update_title(),\n                            self.update_watcher(),\n                            self.update_tab(entity, location, None),\n                        ]);\n                    }\n                }\n            }\n            Message::DndEnterTab(entity, mimes) => {\n                if mimes.iter().all(|m| m.as_str() != \"x-cosmic-files/tab-dnd\") {\n                    self.tab_dnd_hover = Some((entity, Instant::now()));\n                    return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| {\n                        cosmic::Action::App(Message::DndHoverTabTimeout(entity))\n                    });\n                }\n            }\n            Message::DndExitTab => {\n                self.nav_dnd_hover = None;\n            }\n            Message::DndDropTab(entity, data, action) => {\n                self.nav_dnd_hover = None;\n                if let Some((tab, data)) = self.tab_model.data::<Tab>(entity).zip(data) {\n                    let kind = match action {\n                        DndAction::Move => ClipboardKind::Cut { is_dnd: true },\n                        _ => ClipboardKind::Copy,\n                    };\n                    let ret = match &tab.location {\n                        Location::Trash if matches!(action, DndAction::Move) => {\n                            self.delete(data.paths)\n                        }\n                        _ => {\n                            if let Some(path) = tab.location.path_opt() {\n                                self.update(Message::PasteContents(\n                                    path.clone(),\n                                    ClipboardPaste {\n                                        kind,\n                                        paths: data.paths,\n                                    },\n                                ))\n                            } else {\n                                log::warn!(\"{:?} to {:?} is not supported.\", action, tab.location);\n                                Task::none()\n                            }\n                        }\n                    };\n                    return ret;\n                }\n            }\n            Message::DndHoverTabTimeout(entity) => {\n                if self\n                    .tab_dnd_hover\n                    .as_ref()\n                    .is_some_and(|(e, i)| *e == entity && i.elapsed() >= HOVER_DURATION)\n                {\n                    self.tab_dnd_hover = None;\n                    return self.update(Message::TabActivate(entity));\n                }\n            }\n            Message::NavBarClose(entity) => {\n                if let Some(data) = self.nav_model.data::<MounterData>(entity)\n                    && let Some(mounter) = MOUNTERS.get(&data.0)\n                {\n                    return mounter\n                        .unmount(data.1.clone())\n                        .map(|()| cosmic::action::none());\n                }\n            }\n            Message::NavBarContext(entity) => {\n                self.nav_bar_context_id = entity;\n\n                let tab_entity = self.tab_model.active();\n                if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {\n                    // Close location editing if enabled\n                    tab.edit_location = None;\n                    // Close other context menus.\n                    tab.location_context_menu_index = None;\n                    return Task::done(cosmic::Action::App(Message::TabMessage(\n                        Some(tab_entity),\n                        tab::Message::ContextMenu(None, None),\n                    )));\n                }\n            }\n            Message::NavMenuAction(action) => match action {\n                NavMenuAction::ClearRecents => match recently_used_xbel::clear_recently_used() {\n                    Ok(()) => {}\n                    Err(err) => {\n                        log::warn!(\"failed to clear recents history: {}\", err);\n                    }\n                },\n                NavMenuAction::EmptyTrash => {\n                    return self\n                        .push_dialog(DialogPage::EmptyTrash, Some(EMPTY_TRASH_BUTTON_ID.clone()));\n                }\n                NavMenuAction::Open(entity) => {\n                    if let Some(path) = self\n                        .nav_model\n                        .data::<Location>(entity)\n                        .and_then(Location::path_opt)\n                        .cloned()\n                    {\n                        return self.open_file(&[path]);\n                    }\n                }\n                NavMenuAction::OpenWith(entity) => {\n                    if let Some(path) = self\n                        .nav_model\n                        .data::<Location>(entity)\n                        .and_then(Location::path_opt)\n                        .cloned()\n                    {\n                        match tab::item_from_path(&path, IconSizes::default()) {\n                            Ok(item) => {\n                                return self.push_dialog(\n                                    DialogPage::OpenWith {\n                                        path,\n                                        mime: item.mime,\n                                        selected: 0,\n                                        store_opt: \"x-scheme-handler/mime\"\n                                            .parse::<mime_guess::Mime>()\n                                            .ok()\n                                            .and_then(|mime| {\n                                                self.mime_app_cache.get(&mime).first().cloned()\n                                            }),\n                                    },\n                                    None,\n                                );\n                            }\n                            Err(err) => {\n                                log::warn!(\n                                    \"failed to get item for path {}: {}\",\n                                    path.display(),\n                                    err\n                                );\n                            }\n                        }\n                    }\n                }\n                NavMenuAction::RunContextAction(entity, action) => {\n                    if let Some(path) = self\n                        .nav_model\n                        .data::<Location>(entity)\n                        .and_then(Location::path_opt)\n                        .cloned()\n                    {\n                        let paths = vec![path];\n                        if let Some(preset) = self.config.context_actions.get(action) {\n                            if preset.confirm {\n                                return self.push_dialog(\n                                    DialogPage::RunContextAction {\n                                        action,\n                                        paths: paths.into_boxed_slice(),\n                                    },\n                                    Some(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),\n                                );\n                            }\n                            context_action::run(&self.config.context_actions, action, &paths);\n                        } else {\n                            log::warn!(\"invalid context action index `{action}`\");\n                        }\n                    }\n                }\n                NavMenuAction::OpenInNewTab(entity) => {\n                    let open_task = match self.nav_model.data::<Location>(entity) {\n                        Some(Location::Network(uri, display_name, path)) => self.open_tab(\n                            Location::Network(uri.clone(), display_name.clone(), path.clone()),\n                            false,\n                            None,\n                        ),\n                        Some(Location::Path(path)) => {\n                            self.open_tab(Location::Path(path.clone()), false, None)\n                        }\n                        Some(Location::Recents) => self.open_tab(Location::Recents, false, None),\n                        Some(Location::Trash) => self.open_tab(Location::Trash, false, None),\n                        _ => Task::none(),\n                    };\n\n                    return Task::batch([self.close_context_menus(), open_task]);\n                }\n\n                // Open the selected path in a new cosmic-files window.\n                NavMenuAction::OpenInNewWindow(entity) => 'open_in_new_window: {\n                    if let Some(location) = self.nav_model.data::<Location>(entity) {\n                        match env::current_exe() {\n                            Ok(exe) => {\n                                let mut command = process::Command::new(&exe);\n                                match location {\n                                    Location::Path(path) => {\n                                        command.arg(path);\n                                    }\n                                    Location::Trash => {\n                                        command.arg(\"--trash\");\n                                    }\n                                    Location::Network(uri, _, Some(_)) => {\n                                        command.arg(uri);\n                                    }\n                                    Location::Network(..) => {\n                                        command.arg(\"--network\");\n                                    }\n                                    Location::Recents => {\n                                        command.arg(\"--recents\");\n                                    }\n                                    _ => {\n                                        log::error!(\n                                            \"unsupported location for open in new window: {location:?}\"\n                                        );\n                                        break 'open_in_new_window;\n                                    }\n                                }\n                                match command.spawn() {\n                                    Ok(_child) => {}\n                                    Err(err) => {\n                                        log::error!(\"failed to execute {}: {}\", exe.display(), err);\n                                    }\n                                }\n                            }\n                            Err(err) => {\n                                log::error!(\"failed to get current executable path: {err}\");\n                            }\n                        }\n                    }\n                }\n\n                NavMenuAction::Preview(entity) => {\n                    if let Some(path) = self\n                        .nav_model\n                        .data::<Location>(entity)\n                        .and_then(Location::path_opt)\n                    {\n                        match tab::item_from_path(path, IconSizes::default()) {\n                            Ok(item) => {\n                                self.context_page = ContextPage::Preview(\n                                    None,\n                                    PreviewKind::Custom(PreviewItem(Box::new(item))),\n                                );\n                                self.set_show_context(true);\n                            }\n                            Err(err) => {\n                                log::warn!(\n                                    \"failed to get item from path {}: {}\",\n                                    path.display(),\n                                    err\n                                );\n                            }\n                        }\n                    }\n                }\n\n                NavMenuAction::RemoveFromSidebar(entity) => {\n                    if let Some(FavoriteIndex(favorite_i)) =\n                        self.nav_model.data::<FavoriteIndex>(entity)\n                    {\n                        let mut favorites = self.config.favorites.clone();\n                        favorites.remove(*favorite_i);\n                        config_set!(favorites, favorites);\n                        return self.update_config();\n                    }\n                }\n            },\n            Message::Recents => {\n                if self.config.show_recents {\n                    return self.open_tab(Location::Recents, false, None);\n                }\n            }\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            Message::OutputEvent(output_event, output) => {\n                match output_event {\n                    OutputEvent::Created(output_info_opt) => {\n                        let output_id = output.id();\n                        log::info!(\"output {output_id}: created\");\n\n                        let surface_id = WindowId::unique();\n                        if let Some(old_surface_id) =\n                            self.surface_ids.insert(output.clone(), surface_id)\n                        {\n                            //TODO: remove old surface?\n                            log::warn!(\n                                \"output {output_id}: already had surface ID {old_surface_id:?}\"\n                            );\n                        }\n\n                        let display = match output_info_opt {\n                            Some(output_info) => match output_info.name {\n                                Some(output_name) => {\n                                    self.surface_names.insert(surface_id, output_name.clone());\n                                    output_name\n                                }\n                                None => {\n                                    log::warn!(\"output {output_id}: no output name\");\n                                    String::new()\n                                }\n                            },\n                            None => {\n                                log::warn!(\"output {output_id}: no output info\");\n                                String::new()\n                            }\n                        };\n\n                        let (entity, command) = self.open_tab_entity(\n                            Location::Desktop(crate::desktop_dir(), display, self.config.desktop),\n                            false,\n                            None,\n                            widget::Id::unique(),\n                            Some(surface_id),\n                        );\n                        self.windows\n                            .insert(surface_id, Window::new(WindowKind::Desktop(entity)));\n                        return Task::batch([\n                            command,\n                            get_layer_surface(SctkLayerSurfaceSettings {\n                                id: surface_id,\n                                layer: Layer::Bottom,\n                                keyboard_interactivity: KeyboardInteractivity::OnDemand,\n                                input_zone: None,\n                                anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT,\n                                output: IcedOutput::Output(output),\n                                namespace: \"cosmic-files-applet\".into(),\n                                size: Some((None, None)),\n                                margin: IcedMargin {\n                                    top: 0,\n                                    bottom: 0,\n                                    left: 0,\n                                    right: 0,\n                                },\n                                exclusive_zone: 0,\n                                size_limits: Limits::NONE.min_width(1.0).min_height(1.0),\n                            }),\n                            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                            overlap_notify(surface_id, true),\n                        ]);\n                    }\n                    OutputEvent::Removed => {\n                        log::info!(\"output {}: removed\", output.id());\n                        match self.surface_ids.remove(&output) {\n                            Some(surface_id) => {\n                                self.remove_window(&surface_id);\n                                self.surface_names.remove(&surface_id);\n                                return destroy_layer_surface(surface_id);\n                            }\n                            None => {\n                                log::warn!(\"output {}: no surface found\", output.id());\n                            }\n                        }\n                    }\n                    OutputEvent::InfoUpdate(_output_info) => {\n                        log::info!(\"output {}: info update\", output.id());\n                    }\n                }\n            }\n            Message::Cosmic(cosmic) => {\n                // Forward cosmic messages\n                return Task::perform(async move { cosmic }, cosmic::action::cosmic);\n            }\n            Message::None => {}\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            Message::Overlap(w_id, overlap_notify_event) => match overlap_notify_event {\n                OverlapNotifyEvent::OverlapLayerAdd {\n                    identifier,\n                    namespace,\n                    logical_rect,\n                    exclusive,\n                    ..\n                } => {\n                    if exclusive > 0 || namespace == \"Dock\" || namespace == \"Panel\" {\n                        self.overlap.insert(identifier, (w_id, logical_rect));\n                        self.handle_overlap();\n                    }\n                }\n                OverlapNotifyEvent::OverlapLayerRemove { identifier } => {\n                    self.overlap.remove(&identifier);\n                    self.handle_overlap();\n                }\n                _ => {}\n            },\n            Message::Size(window_id, size) => {\n                if self.core.main_window_id() == Some(window_id) {\n                    self.size = Some(size);\n                } else {\n                    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                    self.layer_sizes.insert(window_id, size);\n                }\n            }\n            Message::Eject => {\n                #[cfg(feature = \"gvfs\")]\n                {\n                    let mut paths = self.selected_paths(None);\n                    if let Some(p) = paths.next() {\n                        {\n                            for (k, mounter_items) in &self.mounter_items {\n                                if let Some(mounter) = MOUNTERS.get(k)\n                                    && let Some(item) = mounter_items\n                                        .iter()\n                                        .find(|&item| item.path().is_some_and(|path| path == p))\n                                {\n                                    return mounter\n                                        .unmount(item.clone())\n                                        .map(|()| cosmic::action::none());\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n            Message::Focused(id) => {\n                if let Some(w) = self.windows.get(&id) {\n                    match &w.kind {\n                        WindowKind::Desktop(entity) => self.tab_model.activate(*entity),\n                        _ => {}\n                    };\n                }\n                // Check clipboard when window gains focus\n                // HACK: Wait a moment for the data to be available.\n                return cosmic::task::future(async {\n                    _ = tokio::time::sleep(Duration::from_millis(300)).await;\n                    cosmic::action::app(Message::CheckClipboard)\n                });\n            }\n            Message::Surface(action) => {\n                return cosmic::task::message(cosmic::Action::Cosmic(\n                    cosmic::app::Action::Surface(action),\n                ));\n            }\n            Message::SaveSortNames => {\n                self.must_save_sort_names = false;\n                if let Some(state_handler) = self.state_handler.as_ref()\n                    && let Err(err) = state_handler\n                        .set::<&FxOrderMap<String, (HeadingOptions, bool)>>(\n                            \"sort_names\",\n                            &self.state.sort_names,\n                        )\n                {\n                    log::warn!(\"Failed to save sort names: {err:?}\");\n                }\n            }\n            Message::NetworkDriveOpenEntityAfterMount { entity } => {\n                return self.on_nav_select(entity);\n            }\n            Message::NetworkDriveOpenTabAfterMount { location } => {\n                return self.open_tab(location, false, None);\n            }\n            Message::ReorderTab(ReorderEvent {\n                dragged,\n                target,\n                position,\n            }) => {\n                _ = self.tab_model.reorder(dragged, target, position);\n            }\n        }\n\n        Task::none()\n    }\n\n    fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Message>> {\n        if !self.core.window.show_context {\n            return None;\n        }\n\n        Some(match &self.context_page {\n            ContextPage::About => context_drawer::about(\n                &self.about,\n                |url| Message::LaunchUrl(url.to_string()),\n                Message::ToggleContextPage(ContextPage::About),\n            ),\n            ContextPage::EditHistory => context_drawer::context_drawer(\n                self.edit_history(),\n                Message::ToggleContextPage(ContextPage::EditHistory),\n            )\n            .title(fl!(\"edit-history\")),\n            ContextPage::NetworkDrive => {\n                let mut text_input =\n                    widget::text_input(fl!(\"enter-server-address\"), &self.network_drive_input);\n                let button = if self.network_drive_connecting.is_some() {\n                    widget::button::standard(fl!(\"connecting\"))\n                } else {\n                    text_input = text_input\n                        .on_input(Message::NetworkDriveInput)\n                        .on_submit(|_| Message::NetworkDriveSubmit);\n                    widget::button::standard(fl!(\"connect\")).on_press(Message::NetworkDriveSubmit)\n                };\n                context_drawer::context_drawer(\n                    self.network_drive(),\n                    Message::ToggleContextPage(ContextPage::NetworkDrive),\n                )\n                .title(fl!(\"add-network-drive\"))\n                .header(text_input)\n                .footer(widget::row::with_children([\n                    widget::space::horizontal().into(),\n                    button.into(),\n                ]))\n            }\n            ContextPage::Preview(entity_opt, kind) => {\n                let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());\n                let actions = self\n                    .tab_model\n                    .data::<Tab>(entity)\n                    .and_then(|tab| {\n                        let mut selected = tab.items_opt()?.iter().filter(|item| item.selected);\n\n                        match (selected.next(), selected.next()) {\n                            // Exactly one item\n                            (Some(item), None) => Some(\n                                item.preview_actions()\n                                    .map(move |x| Message::TabMessage(Some(entity), x)),\n                            ),\n                            // Zero or more than one item\n                            _ => None,\n                        }\n                    })\n                    .unwrap_or_else(|| widget::space::horizontal().into());\n                context_drawer::context_drawer(\n                    self.preview(entity_opt, kind, true)\n                        .map(move |x| Message::TabMessage(Some(entity), x)),\n                    Message::ToggleContextPage(ContextPage::Preview(Some(entity), kind.clone())),\n                )\n                .actions(actions)\n            }\n            ContextPage::Settings => context_drawer::context_drawer(\n                self.settings(),\n                Message::ToggleContextPage(ContextPage::Settings),\n            )\n            .title(fl!(\"settings\")),\n        })\n    }\n\n    fn dialog(&self) -> Option<Element<'_, Message>> {\n        //TODO: should gallery view just be a dialog?\n        let entity = self.tab_model.active();\n        if let Some(tab) = self.tab_model.data::<Tab>(entity)\n            && tab.gallery\n        {\n            return Some(\n                tab.gallery_view()\n                    .map(move |x| Message::TabMessage(Some(entity), x)),\n            );\n        }\n        let dialog_page = self.dialog_pages.front()?;\n\n        let cosmic_theme::Spacing {\n            space_xxs, space_s, ..\n        } = theme::spacing();\n\n        let dialog = match dialog_page {\n            DialogPage::Compress {\n                paths,\n                to,\n                name,\n                archive_type,\n                password,\n            } => {\n                let mut dialog = widget::dialog().title(fl!(\"create-archive\"));\n\n                let complete_maybe = if name.is_empty() {\n                    None\n                } else if name == \".\" || name == \"..\" {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\n                        \"name-invalid\",\n                        filename = name.as_str()\n                    )));\n                    None\n                } else if name.contains('/') {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-no-slashes\")));\n                    None\n                } else {\n                    let extension = archive_type.extension();\n                    let name = format!(\"{name}{extension}\");\n                    let path = to.join(&name);\n                    if path.exists() {\n                        dialog =\n                            dialog.tertiary_action(widget::text::body(fl!(\"file-already-exists\")));\n                        None\n                    } else {\n                        if name.starts_with('.') {\n                            dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-hidden\")));\n                        }\n                        Some(Message::DialogComplete)\n                    }\n                };\n\n                let archive_types = ArchiveType::all();\n                let selected = archive_types.iter().position(|&x| x == *archive_type);\n                dialog = dialog\n                    .primary_action(\n                        widget::button::suggested(fl!(\"create\"))\n                            .on_press_maybe(complete_maybe.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(\n                        widget::column::with_children([\n                            widget::text::body(fl!(\"file-name\")).into(),\n                            widget::row::with_children([\n                                widget::text_input(\"\", name.as_str())\n                                    .id(self.dialog_text_input.clone())\n                                    .on_input(move |name| {\n                                        Message::DialogUpdate(DialogPage::Compress {\n                                            paths: paths.clone(),\n                                            to: to.clone(),\n                                            name,\n                                            archive_type: *archive_type,\n                                            password: password.clone(),\n                                        })\n                                    })\n                                    .on_submit_maybe(\n                                        complete_maybe.clone().map(|maybe| move |_| maybe.clone()),\n                                    )\n                                    .into(),\n                                Element::from(widget::dropdown(\n                                    archive_types,\n                                    selected,\n                                    move |index| index,\n                                ))\n                                .map(|index| {\n                                    Message::DialogUpdate(DialogPage::Compress {\n                                        paths: paths.clone(),\n                                        to: to.clone(),\n                                        name: name.clone(),\n                                        archive_type: archive_types[index],\n                                        password: password.clone(),\n                                    })\n                                }),\n                            ])\n                            .align_y(Alignment::Center)\n                            .spacing(space_xxs)\n                            .into(),\n                        ])\n                        .spacing(space_xxs),\n                    );\n\n                if *archive_type == ArchiveType::Zip {\n                    let password_unwrapped = password.clone().unwrap_or_default();\n                    dialog = dialog.control(widget::column::with_children([\n                        widget::text::body(fl!(\"password\")).into(),\n                        widget::text_input(\"\", password_unwrapped)\n                            .password()\n                            .on_input(move |password_unwrapped| {\n                                Message::DialogUpdate(DialogPage::Compress {\n                                    paths: paths.clone(),\n                                    to: to.clone(),\n                                    name: name.clone(),\n                                    archive_type: *archive_type,\n                                    password: Some(password_unwrapped),\n                                })\n                            })\n                            .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))\n                            .into(),\n                    ]));\n                }\n\n                dialog\n            }\n            DialogPage::EmptyTrash => widget::dialog()\n                .title(fl!(\"empty-trash-title\"))\n                .body(fl!(\"empty-trash-warning\"))\n                .primary_action(\n                    widget::button::suggested(fl!(\"empty-trash\"))\n                        .on_press(Message::DialogComplete)\n                        .id(EMPTY_TRASH_BUTTON_ID.clone()),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                ),\n            DialogPage::FailedOperation(id) => {\n                //TODO: try next dialog page (making sure index is used by Dialog messages)?\n                let (operation, _, err) = self.failed_operations.get(id)?;\n\n                //TODO: nice description of error\n                widget::dialog()\n                    .title(\"Failed operation\")\n                    .body(format!(\"{operation:#?}\\n{err}\"))\n                    .icon(icon::from_name(\"dialog-error\").size(64))\n                    //TODO: retry action\n                    .primary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n            }\n            DialogPage::FailedOperations(ids) => {\n                let errors: Vec<String> = ids\n                    .iter()\n                    .filter_map(|id| match self.failed_operations.get(id) {\n                        Some((operation, _, err)) => Some(format!(\"{operation:#?}\\n{err}\")),\n                        _ => None,\n                    })\n                    .collect();\n\n                //TODO: nice description of error\n                widget::dialog()\n                    .title(\"Failed operations\")\n                    .body(errors.join(\"\\n\\n\"))\n                    .icon(icon::from_name(\"dialog-error\").size(64))\n                    //TODO: retry action\n                    .primary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n            }\n            DialogPage::ExtractPassword { id, password } => widget::dialog()\n                .title(fl!(\"extract-password-required\"))\n                .icon(icon::from_name(\"dialog-error\").size(64))\n                .control(\n                    widget::text_input(\"\", password)\n                        .password()\n                        .on_input(move |password| {\n                            Message::DialogUpdate(DialogPage::ExtractPassword { id: *id, password })\n                        })\n                        .on_submit(|_| Message::DialogComplete)\n                        .id(self.dialog_text_input.clone()),\n                )\n                .primary_action(\n                    widget::button::suggested(fl!(\"extract-here\"))\n                        .on_press(Message::DialogComplete),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                ),\n            DialogPage::MountError {\n                mounter_key: _,\n                item: _,\n                error,\n            } => widget::dialog()\n                .title(fl!(\"mount-error\"))\n                .body(error)\n                .icon(icon::from_name(\"dialog-error\").size(64))\n                .primary_action(\n                    widget::button::standard(fl!(\"try-again\"))\n                        .on_press(Message::DialogComplete)\n                        .id(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                ),\n            DialogPage::NetworkAuth {\n                mounter_key,\n                uri,\n                auth,\n                auth_tx,\n            } => {\n                //TODO: use URI!\n                let mut controls = widget::column::with_capacity(4);\n                let mut id_assigned = false;\n\n                if let Some(username) = &auth.username_opt {\n                    //TODO: what should submit do?\n                    let mut input = widget::text_input(fl!(\"username\"), username)\n                        .on_input(move |value| {\n                            Message::DialogUpdate(DialogPage::NetworkAuth {\n                                mounter_key: *mounter_key,\n                                uri: uri.clone(),\n                                auth: MounterAuth {\n                                    username_opt: Some(value),\n                                    ..auth.clone()\n                                },\n                                auth_tx: auth_tx.clone(),\n                            })\n                        })\n                        .on_submit(|_| Message::DialogComplete);\n                    if !id_assigned {\n                        input = input.id(self.dialog_text_input.clone());\n                        id_assigned = true;\n                    }\n                    controls = controls.push(input);\n                }\n\n                if let Some(domain) = &auth.domain_opt {\n                    //TODO: what should submit do?\n                    let mut input = widget::text_input(fl!(\"domain\"), domain)\n                        .on_input(move |value| {\n                            Message::DialogUpdate(DialogPage::NetworkAuth {\n                                mounter_key: *mounter_key,\n                                uri: uri.clone(),\n                                auth: MounterAuth {\n                                    domain_opt: Some(value),\n                                    ..auth.clone()\n                                },\n                                auth_tx: auth_tx.clone(),\n                            })\n                        })\n                        .on_submit(|_| Message::DialogComplete);\n                    if !id_assigned {\n                        input = input.id(self.dialog_text_input.clone());\n                        id_assigned = true;\n                    }\n                    controls = controls.push(input);\n                }\n\n                if let Some(password) = &auth.password_opt {\n                    //TODO: what should submit do?\n                    //TODO: button for showing password\n                    let mut input = widget::secure_input(fl!(\"password\"), password, None, true)\n                        .on_input(move |value| {\n                            Message::DialogUpdate(DialogPage::NetworkAuth {\n                                mounter_key: *mounter_key,\n                                uri: uri.clone(),\n                                auth: MounterAuth {\n                                    password_opt: Some(value),\n                                    ..auth.clone()\n                                },\n                                auth_tx: auth_tx.clone(),\n                            })\n                        })\n                        .on_submit(|_| Message::DialogComplete);\n                    if !id_assigned {\n                        input = input.id(self.dialog_text_input.clone());\n                    }\n                    controls = controls.push(input);\n                }\n\n                if let Some(remember) = &auth.remember_opt {\n                    //TODO: what should submit do?\n                    //TODO: button for showing password\n                    controls = controls.push(\n                        widget::checkbox(*remember)\n                            .label(fl!(\"remember-password\"))\n                            .on_toggle(move |value| {\n                                Message::DialogUpdate(DialogPage::NetworkAuth {\n                                    mounter_key: *mounter_key,\n                                    uri: uri.clone(),\n                                    auth: MounterAuth {\n                                        remember_opt: Some(value),\n                                        ..auth.clone()\n                                    },\n                                    auth_tx: auth_tx.clone(),\n                                })\n                            }),\n                    );\n                }\n\n                let mut parts = auth.message.splitn(2, '\\n');\n                let title = parts.next().unwrap_or_default();\n                let body = parts.next().unwrap_or_default();\n\n                let mut widget = widget::dialog()\n                    .title(title)\n                    .body(body)\n                    .control(controls.spacing(space_s))\n                    .primary_action(\n                        widget::button::suggested(fl!(\"connect\")).on_press(Message::DialogComplete),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    );\n\n                if let Some(_anonymous) = &auth.anonymous_opt {\n                    widget = widget.tertiary_action(\n                        widget::button::text(fl!(\"connect-anonymously\")).on_press(\n                            Message::DialogUpdateComplete(DialogPage::NetworkAuth {\n                                mounter_key: *mounter_key,\n                                uri: uri.clone(),\n                                auth: MounterAuth {\n                                    anonymous_opt: Some(true),\n                                    ..auth.clone()\n                                },\n                                auth_tx: auth_tx.clone(),\n                            }),\n                        ),\n                    );\n                }\n\n                widget\n            }\n            DialogPage::NetworkError {\n                mounter_key: _,\n                uri: _,\n                error,\n            } => widget::dialog()\n                .title(fl!(\"network-drive-error\"))\n                .body(error)\n                .icon(icon::from_name(\"dialog-error\").size(64))\n                .primary_action(\n                    widget::button::standard(fl!(\"try-again\")).on_press(Message::DialogComplete),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                ),\n            DialogPage::NewItem { parent, name, dir } => {\n                let mut dialog = widget::dialog().title(if *dir {\n                    fl!(\"create-new-folder\")\n                } else {\n                    fl!(\"create-new-file\")\n                });\n\n                let complete_maybe = if name.is_empty() {\n                    None\n                } else if name == \".\" || name == \"..\" {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\n                        \"name-invalid\",\n                        filename = name.as_str()\n                    )));\n                    None\n                } else if name.contains('/') {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-no-slashes\")));\n                    None\n                } else {\n                    let path = parent.join(name);\n                    if path.exists() {\n                        if path.is_dir() {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"folder-already-exists\")));\n                        } else {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"file-already-exists\")));\n                        }\n                        None\n                    } else {\n                        if name.starts_with('.') {\n                            dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-hidden\")));\n                        }\n                        Some(Message::DialogComplete)\n                    }\n                };\n\n                dialog\n                    .primary_action(\n                        widget::button::suggested(fl!(\"save\"))\n                            .on_press_maybe(complete_maybe.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(\n                        widget::column::with_children([\n                            widget::text::body(if *dir {\n                                fl!(\"folder-name\")\n                            } else {\n                                fl!(\"file-name\")\n                            })\n                            .into(),\n                            widget::text_input(\"\", name.as_str())\n                                .id(self.dialog_text_input.clone())\n                                .on_input(move |name| {\n                                    Message::DialogUpdate(DialogPage::NewItem {\n                                        parent: parent.clone(),\n                                        name,\n                                        dir: *dir,\n                                    })\n                                })\n                                .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))\n                                .into(),\n                        ])\n                        .spacing(space_xxs),\n                    )\n            }\n            DialogPage::RunContextAction { action, paths } => {\n                let name = self\n                    .config\n                    .context_actions\n                    .get(*action)\n                    .map_or_else(|| fl!(\"context-action\"), |preset| preset.name.clone());\n\n                widget::dialog()\n                    .title(fl!(\"context-action-confirm-title\", name = name))\n                    .body(fl!(\"context-action-confirm-warning\", items = paths.len()))\n                    .icon(icon::from_name(\"dialog-error\").size(64))\n                    .primary_action(\n                        widget::button::suggested(fl!(\"run\"))\n                            .on_press(Message::DialogComplete)\n                            .id(CONFIRM_CONTEXT_ACTION_BUTTON_ID.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n            }\n            DialogPage::OpenWith {\n                path,\n                mime,\n                selected,\n                store_opt,\n                ..\n            } => {\n                let name = match path.file_name() {\n                    Some(file_name) => file_name.to_str(),\n                    None => path.as_os_str().to_str(),\n                };\n\n                let mut column = widget::list_column();\n                let available_apps = self.get_apps_for_mime(mime);\n                let item_height = 32.0;\n                let mut displayed_default = false;\n                let mut last_kind = MimeAppMatch::Exact;\n                for (i, &(app, kind)) in available_apps.iter().enumerate() {\n                    if kind != last_kind {\n                        match kind {\n                            MimeAppMatch::Related => {\n                                column = column.add(widget::text::heading(fl!(\"related-apps\")));\n                            }\n                            MimeAppMatch::Other => {\n                                column = column.add(widget::text::heading(fl!(\"other-apps\")));\n                            }\n                            _ => {}\n                        }\n                        last_kind = kind;\n                    }\n                    column = column.add(\n                        widget::mouse_area(\n                            widget::button::custom(\n                                widget::row::with_children([\n                                    icon(app.icon.clone()).size(32).into(),\n                                    if app.is_default && !displayed_default {\n                                        displayed_default = true;\n                                        widget::text::body(fl!(\n                                            \"default-app\",\n                                            name = Some(app.name.as_str())\n                                        ))\n                                        .into()\n                                    } else {\n                                        widget::text::body(app.name.clone()).into()\n                                    },\n                                    widget::space::horizontal().into(),\n                                    if *selected == i {\n                                        icon::from_name(\"checkbox-checked-symbolic\").size(16).into()\n                                    } else {\n                                        widget::space::horizontal()\n                                            .width(Length::Fixed(16.0))\n                                            .into()\n                                    },\n                                ])\n                                .spacing(space_s)\n                                .height(Length::Fixed(item_height))\n                                .align_y(Alignment::Center),\n                            )\n                            .width(Length::Fill)\n                            .class(theme::Button::MenuItem)\n                            .force_enabled(true),\n                        )\n                        .on_press(Message::OpenWithSelection(i))\n                        .on_double_press(Message::DialogComplete),\n                    );\n                }\n\n                let mut dialog = widget::dialog()\n                    .title(fl!(\"open-with-title\", name = name))\n                    .primary_action(\n                        widget::button::suggested(fl!(\"open\"))\n                            .on_press(Message::DialogComplete)\n                            .id(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(widget::scrollable(column).height({\n                        let max_size = self\n                            .size\n                            .map_or(480.0, |size| (size.height - 256.0).min(480.0));\n                        // (32 (item_height) + 5.0 (custom button padding)) + (space_xxs (list item spacing) * 2)\n                        let scrollable_height = available_apps.len() as f32\n                            * f32::from(space_xxs).mul_add(2.0, item_height + 5.0);\n\n                        if scrollable_height > max_size {\n                            Length::Fixed(max_size)\n                        } else {\n                            Length::Shrink\n                        }\n                    }));\n\n                if let Some(app) = store_opt {\n                    dialog = dialog.tertiary_action(\n                        widget::button::text(fl!(\"browse-store\", store = app.name.as_str()))\n                            .on_press(Message::OpenWithBrowse),\n                    );\n                }\n\n                dialog\n            }\n            DialogPage::PermanentlyDelete { paths } => {\n                let target = if paths.len() == 1 {\n                    format!(\n                        \"\\\"{}\\\"\",\n                        paths[0].file_name().map_or_else(\n                            || paths[0].to_string_lossy(),\n                            std::ffi::OsStr::to_string_lossy\n                        )\n                    )\n                } else {\n                    fl!(\"selected-items\", items = paths.len())\n                };\n\n                widget::dialog()\n                    .title(fl!(\"permanently-delete-question\"))\n                    .primary_action(\n                        widget::button::destructive(fl!(\"delete\"))\n                            .on_press(Message::DialogComplete)\n                            .id(PERMANENT_DELETE_BUTTON_ID.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(widget::text(fl!(\n                        \"permanently-delete-warning\",\n                        target = target\n                    )))\n            }\n            DialogPage::DeleteTrash { items } => {\n                let target = if items.len() == 1 {\n                    format!(\"\\\"{}\\\"\", items[0].name.to_string_lossy())\n                } else {\n                    fl!(\"selected-items\", items = items.len())\n                };\n\n                widget::dialog()\n                    .title(fl!(\"permanently-delete-question\"))\n                    .primary_action(\n                        widget::button::destructive(fl!(\"delete\"))\n                            .on_press(Message::DialogComplete)\n                            .id(DELETE_TRASH_BUTTON_ID.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(widget::text(fl!(\n                        \"permanently-delete-warning\",\n                        target = target\n                    )))\n            }\n            DialogPage::RenameItem {\n                from,\n                parent,\n                name,\n                dir,\n            } => {\n                //TODO: combine logic with NewItem\n                let mut dialog = widget::dialog().title(if *dir {\n                    fl!(\"rename-folder\")\n                } else {\n                    fl!(\"rename-file\")\n                });\n\n                let complete_maybe = if name.is_empty() {\n                    None\n                } else if name == \".\" || name == \"..\" {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\n                        \"name-invalid\",\n                        filename = name.as_str()\n                    )));\n                    None\n                } else if name.contains('/') {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-no-slashes\")));\n                    None\n                } else {\n                    let path = parent.join(name);\n                    if *from != path && path.exists() {\n                        if path.is_dir() {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"folder-already-exists\")));\n                        } else {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"file-already-exists\")));\n                        }\n                        None\n                    } else {\n                        if name.starts_with('.') {\n                            dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-hidden\")));\n                        }\n                        Some(Message::DialogComplete)\n                    }\n                };\n\n                dialog\n                    .primary_action(\n                        widget::button::suggested(fl!(\"rename\"))\n                            .on_press_maybe(complete_maybe.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(\n                        widget::column::with_children([\n                            widget::text::body(if *dir {\n                                fl!(\"folder-name\")\n                            } else {\n                                fl!(\"file-name\")\n                            })\n                            .into(),\n                            widget::text_input(\"\", name.as_str())\n                                .id(self.dialog_text_input.clone())\n                                .double_click_select_delimiter('.')\n                                .on_input(move |name| {\n                                    Message::DialogUpdate(DialogPage::RenameItem {\n                                        from: from.clone(),\n                                        parent: parent.clone(),\n                                        name,\n                                        dir: *dir,\n                                    })\n                                })\n                                .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))\n                                .into(),\n                        ])\n                        .spacing(space_xxs),\n                    )\n            }\n            DialogPage::Replace {\n                from,\n                to,\n                multiple,\n                apply_to_all,\n                conflict_count,\n                tx,\n            } => {\n                let military_time = self.config.tab.military_time;\n                let dialog = widget::dialog()\n                    .title(fl!(\"replace-title\", filename = to.name.as_str()))\n                    .body(fl!(\"replace-warning-operation\"))\n                    .control(\n                        to.replace_view(fl!(\"original-file\"), military_time)\n                            .map(|x| Message::TabMessage(None, x)),\n                    )\n                    .control(\n                        from.replace_view(fl!(\"replace-with\"), military_time)\n                            .map(|x| Message::TabMessage(None, x)),\n                    )\n                    .primary_action(\n                        widget::button::suggested(fl!(\"replace\"))\n                            .on_press(Message::ReplaceResult(ReplaceResult::Replace(\n                                *apply_to_all,\n                            )))\n                            .id(REPLACE_BUTTON_ID.clone()),\n                    );\n                if *multiple {\n                    dialog\n                        .control(\n                            widget::checkbox(*apply_to_all)\n                                .label(format!(\"{} ({})\", fl!(\"apply-to-all\"), *conflict_count))\n                                .on_toggle(|apply_to_all| {\n                                    Message::DialogUpdate(DialogPage::Replace {\n                                        from: from.clone(),\n                                        to: to.clone(),\n                                        multiple: *multiple,\n                                        apply_to_all,\n                                        conflict_count: *conflict_count,\n                                        tx: tx.clone(),\n                                    })\n                                }),\n                        )\n                        .secondary_action(\n                            widget::button::standard(fl!(\"skip\")).on_press(Message::ReplaceResult(\n                                ReplaceResult::Skip(*apply_to_all),\n                            )),\n                        )\n                        .tertiary_action(\n                            widget::button::text(fl!(\"cancel\"))\n                                .on_press(Message::ReplaceResult(ReplaceResult::Cancel)),\n                        )\n                } else {\n                    dialog\n                        .secondary_action(\n                            widget::button::standard(fl!(\"cancel\"))\n                                .on_press(Message::ReplaceResult(ReplaceResult::Cancel)),\n                        )\n                        .tertiary_action(\n                            widget::button::text(fl!(\"keep-both\"))\n                                .on_press(Message::ReplaceResult(ReplaceResult::KeepBoth)),\n                        )\n                }\n            }\n            DialogPage::SetExecutableAndLaunch { path } => {\n                let name = match path.file_name() {\n                    Some(file_name) => file_name.to_str(),\n                    None => path.as_os_str().to_str(),\n                };\n                widget::dialog()\n                    .title(fl!(\"set-executable-and-launch\"))\n                    .primary_action(\n                        widget::button::text(fl!(\"set-and-launch\"))\n                            .class(theme::Button::Suggested)\n                            .on_press(Message::DialogComplete)\n                            .id(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::text(fl!(\"cancel\"))\n                            .class(theme::Button::Standard)\n                            .on_press(Message::DialogCancel),\n                    )\n                    .control(widget::text::text(fl!(\n                        \"set-executable-and-launch-description\",\n                        name = name\n                    )))\n            }\n            DialogPage::FavoritePathError { path, .. } => widget::dialog()\n                .title(fl!(\"favorite-path-error\"))\n                .body(fl!(\n                    \"favorite-path-error-description\",\n                    path = path.as_os_str().to_str()\n                ))\n                .icon(icon::from_name(\"dialog-error\").size(64))\n                .primary_action(\n                    widget::button::destructive(fl!(\"remove\"))\n                        .on_press(Message::DialogComplete)\n                        .id(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"keep\")).on_press(Message::DialogCancel),\n                ),\n        };\n        Some(dialog.into())\n    }\n\n    fn footer(&self) -> Option<Element<'_, Message>> {\n        if self.progress_operations.is_empty() {\n            return None;\n        }\n\n        let cosmic_theme::Spacing {\n            space_xs, space_s, ..\n        } = theme::spacing();\n\n        let mut title = String::new();\n        let mut total_progress = 0.0;\n        let mut count = 0;\n        let mut all_paused = true;\n        for (op, controller) in self.pending_operations.values() {\n            if !controller.is_paused() {\n                all_paused = false;\n            }\n            if op.show_progress_notification() {\n                let progress = controller.progress();\n                if title.is_empty() {\n                    title = op.pending_text(progress, controller.state());\n                }\n                total_progress += progress;\n                count += 1;\n            }\n        }\n        let running = count;\n        // Adjust the progress bar so it does not jump around when operations finish\n        for id in &self.progress_operations {\n            if self.complete_operations.contains_key(id) {\n                total_progress += 1.0;\n                count += 1;\n            }\n        }\n        let finished = count - running;\n        total_progress /= count as f32;\n        if running >= 1 && (running > 1 || finished > 0) {\n            if finished > 0 {\n                title = fl!(\n                    \"operations-running-finished\",\n                    running = running,\n                    finished = finished,\n                    percent = ((total_progress * 100.0) as i32)\n                );\n            } else {\n                title = fl!(\n                    \"operations-running\",\n                    running = running,\n                    percent = ((total_progress * 100.0) as i32)\n                );\n            }\n        }\n\n        //TODO: get height from theme?\n        let progress_bar_height = Length::Fixed(4.0);\n        let progress_bar = widget::determinate_linear(total_progress)\n            .width(Length::Fill)\n            .girth(progress_bar_height);\n\n        let container = widget::layer_container(widget::column::with_children([\n            widget::row::with_children([\n                progress_bar.into(),\n                if all_paused {\n                    widget::tooltip(\n                        widget::button::icon(icon::from_name(\"media-playback-start-symbolic\"))\n                            .on_press(Message::PendingPauseAll(false))\n                            .padding(8),\n                        widget::text::body(fl!(\"resume\")),\n                        widget::tooltip::Position::Top,\n                    )\n                    .into()\n                } else {\n                    widget::tooltip(\n                        widget::button::icon(icon::from_name(\"media-playback-pause-symbolic\"))\n                            .on_press(Message::PendingPauseAll(true))\n                            .padding(8),\n                        widget::text::body(fl!(\"pause\")),\n                        widget::tooltip::Position::Top,\n                    )\n                    .into()\n                },\n                widget::tooltip(\n                    widget::button::icon(icon::from_name(\"window-close-symbolic\"))\n                        .on_press(Message::PendingCancelAll)\n                        .padding(8),\n                    widget::text::body(fl!(\"cancel\")),\n                    widget::tooltip::Position::Top,\n                )\n                .into(),\n            ])\n            .align_y(Alignment::Center)\n            .into(),\n            widget::text::body(title).into(),\n            widget::space::vertical().height(space_s).into(),\n            widget::row::with_children([\n                widget::button::link(fl!(\"details\"))\n                    .on_press(Message::ToggleContextPage(ContextPage::EditHistory))\n                    .padding(0)\n                    .trailing_icon(true)\n                    .into(),\n                widget::space::horizontal().into(),\n                widget::button::standard(fl!(\"dismiss\"))\n                    .on_press(Message::PendingDismiss)\n                    .into(),\n            ])\n            .align_y(Alignment::Center)\n            .into(),\n        ]))\n        .padding([8, space_xs])\n        .layer(cosmic_theme::Layer::Primary);\n\n        Some(container.into())\n    }\n\n    fn header_start(&self) -> Vec<Element<'_, Self::Message>> {\n        vec![menu::menu_bar(\n            &self.core,\n            self.tab_model.active_data::<Tab>(),\n            &self.config,\n            &self.modifiers,\n            &self.key_binds,\n            self.clipboard_has_content(),\n        )]\n    }\n\n    fn header_end(&self) -> Vec<Element<'_, Self::Message>> {\n        let mut elements = Vec::with_capacity(2);\n\n        if let Some(term) = self.search_get() {\n            if self.core.is_condensed() {\n                elements.push(\n                    //TODO: selected state is not appearing different\n                    widget::button::icon(icon::from_name(\"system-search-symbolic\"))\n                        .on_press(Message::SearchClear)\n                        .padding(8)\n                        .selected(true)\n                        .into(),\n                );\n            } else {\n                elements.push(\n                    widget::text_input::search_input(\"\", term)\n                        .width(Length::Fixed(240.0))\n                        .id(self.search_id.clone())\n                        .on_clear(Message::SearchClear)\n                        .on_input(Message::SearchInput)\n                        .into(),\n                );\n            }\n        } else {\n            elements.push(\n                widget::button::icon(icon::from_name(\"system-search-symbolic\"))\n                    .on_press(Message::SearchActivate)\n                    .padding(8)\n                    .into(),\n            );\n        }\n\n        elements\n    }\n\n    /// Creates a view after each update.\n    fn view(&self) -> Element<'_, Self::Message> {\n        let cosmic_theme::Spacing {\n            space_xxs, space_s, ..\n        } = theme::spacing();\n\n        let mut tab_column = widget::column::with_capacity(4);\n\n        if self.core.is_condensed()\n            && let Some(term) = self.search_get()\n        {\n            tab_column = tab_column.push(\n                widget::container(\n                    widget::text_input::search_input(\"\", term)\n                        .width(Length::Fill)\n                        .id(self.search_id.clone())\n                        .on_clear(Message::SearchClear)\n                        .on_input(Message::SearchInput),\n                )\n                .padding(space_xxs),\n            );\n        }\n\n        if self.tab_model.len() > 1 {\n            tab_column = tab_column.push(\n                widget::container(\n                    widget::tab_bar::horizontal(&self.tab_model)\n                        .button_height(32)\n                        .button_spacing(space_xxs)\n                        .enable_tab_drag(String::from(\"x-cosmic-files/tab-dnd\"))\n                        .on_reorder(Message::ReorderTab)\n                        .tab_drag_threshold(25.)\n                        .on_activate(Message::TabActivate)\n                        .on_close(|entity| Message::TabClose(Some(entity)))\n                        .on_dnd_enter(Message::DndEnterTab)\n                        .on_dnd_leave(|_| Message::DndExitTab)\n                        .on_dnd_drop(|entity, data, action| {\n                            Message::DndDropTab(entity, data, action)\n                        })\n                        .drag_id(self.tab_drag_id),\n                )\n                .class(style::Container::Background)\n                .width(Length::Fill)\n                .padding([0, space_s]),\n            );\n        }\n\n        let entity = self.tab_model.active();\n        if let Some(tab) = self.tab_model.data::<Tab>(entity) {\n            let tab_view = tab\n                .view(\n                    &self.key_binds,\n                    &self.modifiers,\n                    self.clipboard_has_content(),\n                    &self.config.context_actions,\n                )\n                .map(move |message| Message::TabMessage(Some(entity), message));\n            tab_column = tab_column.push(tab_view);\n        } else {\n            //TODO\n        }\n\n        // The toaster is added on top of an empty element to ensure that it does not override context menus\n        tab_column = tab_column.push(widget::toaster(&self.toasts, widget::space::horizontal()));\n\n        let content: Element<_> = tab_column.into();\n\n        // Uncomment to debug layout:\n        //content.explain(cosmic::iced::Color::WHITE)\n        content\n    }\n\n    fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> {\n        let content = match self.windows.get(&id) {\n            Some(window) => match &window.kind {\n                WindowKind::ContextMenu(entity, id) => match self.tab_model.data::<Tab>(*entity) {\n                    Some(tab) => {\n                        return widget::autosize::autosize(\n                            menu::context_menu(\n                                tab,\n                                &self.key_binds,\n                                &window.modifiers,\n                                self.clipboard_has_content(),\n                                &self.config.context_actions,\n                            )\n                            .map(|x| Message::TabMessage(Some(*entity), x)),\n                            id.clone(),\n                        )\n                        .into();\n                    }\n                    None => widget::text(\"Unknown tab ID\").into(),\n                },\n                WindowKind::Desktop(entity) => {\n                    let mut tab_column = widget::column::with_capacity(3);\n\n                    let tab_view = match self.tab_model.data::<Tab>(*entity) {\n                        Some(tab) => tab\n                            .view(\n                                &self.key_binds,\n                                &window.modifiers,\n                                self.clipboard_has_content(),\n                                &self.config.context_actions,\n                            )\n                            .map(move |message| Message::TabMessage(Some(*entity), message)),\n                        None => widget::space::vertical().into(),\n                    };\n\n                    tab_column = tab_column.push(tab_view);\n\n                    // The toaster is added on top of an empty element to ensure that it does not override context menus\n                    tab_column =\n                        tab_column.push(widget::toaster(&self.toasts, widget::space::horizontal()));\n                    return if let Some(margin) = self.margin.get(&id) {\n                        if margin.0 >= 0. || margin.2 >= 0. {\n                            tab_column = widget::column::with_children([\n                                space::vertical().height(margin.0).into(),\n                                tab_column.into(),\n                                space::vertical().height(margin.2).into(),\n                            ]);\n                        }\n                        if margin.1 >= 0. || margin.3 >= 0. {\n                            Element::from(widget::row::with_children([\n                                space::horizontal().width(margin.1).into(),\n                                tab_column.into(),\n                                space::horizontal().width(margin.3).into(),\n                            ]))\n                        } else {\n                            tab_column.into()\n                        }\n                    } else {\n                        tab_column.into()\n                    };\n                }\n                WindowKind::DesktopViewOptions => self.desktop_view_options(),\n                WindowKind::Dialogs(id) => match self.dialog() {\n                    Some(element) => return widget::autosize::autosize(element, id.clone()).into(),\n                    None => widget::space::horizontal().into(),\n                },\n                WindowKind::Preview(entity_opt, kind) => self\n                    .preview(entity_opt, kind, false)\n                    .map(|x| Message::TabMessage(*entity_opt, x)),\n                WindowKind::FileDialog(..) => match &self.file_dialog_opt {\n                    Some(dialog) => return dialog.view(id),\n                    None => widget::text(\"Unknown window ID\").into(),\n                },\n            },\n            None => {\n                //TODO: distinct views per monitor in desktop mode\n                return self.view_main().map(|message| match message {\n                    cosmic::Action::App(app) => app,\n                    cosmic::Action::Cosmic(cosmic) => Message::Cosmic(cosmic),\n                    cosmic::Action::None => Message::None,\n                });\n            }\n        };\n\n        widget::container(widget::id_container(\n            widget::scrollable(content),\n            widget::Id::new(\"main container for files\"),\n        ))\n        .width(Length::Fill)\n        .height(Length::Fill)\n        .class(theme::Container::WindowBackground)\n        .into()\n    }\n\n    fn system_theme_update(\n        &mut self,\n        _keys: &[&'static str],\n        _new_theme: &cosmic::cosmic_theme::Theme,\n    ) -> Task<Self::Message> {\n        self.update(Message::SystemThemeModeChange)\n    }\n\n    fn subscription(&self) -> Subscription<Self::Message> {\n        struct WatcherSubscription;\n        struct TrashWatcherSubscription;\n        struct TimeSubscription;\n        #[cfg(all(\n            not(feature = \"desktop-applet\"),\n            not(target_os = \"ios\"),\n            not(target_os = \"android\")\n        ))]\n        struct RecentsWatcherSubscription;\n\n        let mut subscriptions = vec![\n            //TODO: filter more events by window id\n            event::listen_with(|event, status, window_id| match event {\n                Event::Mouse(mouse::Event::ButtonPressed(button)) => match status {\n                    event::Status::Ignored => Some(Message::Mouse(window_id, button)),\n                    event::Status::Captured => None,\n                },\n                Event::Keyboard(KeyEvent::KeyPressed {\n                    key,\n                    modifiers,\n                    text,\n                    ..\n                }) => match status {\n                    event::Status::Ignored => Some(Message::Key(window_id, modifiers, key, text)),\n                    event::Status::Captured => None,\n                },\n                Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {\n                    Some(Message::ModifiersChanged(window_id, modifiers))\n                }\n                #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                Event::Window(WindowEvent::Focused) => Some(Message::Focused(window_id)),\n                #[cfg(not(all(feature = \"wayland\", feature = \"desktop-applet\")))]\n                Event::Window(WindowEvent::Focused) => Some(Message::CheckClipboard),\n                Event::Window(WindowEvent::CloseRequested) => Some(Message::WindowClose),\n                Event::Window(WindowEvent::Opened { position: _, size }) => {\n                    Some(Message::Size(window_id, size))\n                }\n                Event::Window(WindowEvent::Resized(s)) => Some(Message::Size(window_id, s)),\n                #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n                Event::PlatformSpecific(event::PlatformSpecific::Wayland(wayland_event)) => {\n                    match wayland_event {\n                        WaylandEvent::Output(output_event, output) => {\n                            Some(Message::OutputEvent(output_event, output))\n                        }\n                        #[cfg(feature = \"desktop\")]\n                        WaylandEvent::OverlapNotify(event, ..) => {\n                            Some(Message::Overlap(window_id, event))\n                        }\n                        _ => None,\n                    }\n                }\n                _ => None,\n            }),\n            Config::subscription().map(|update| {\n                if !update.errors.is_empty() {\n                    log::info!(\n                        \"errors loading config {:?}: {:?}\",\n                        update.keys,\n                        update.errors\n                    );\n                }\n                Message::Config(update.config)\n            }),\n            cosmic_config::config_subscription::<_, TimeConfig>(\n                TypeId::of::<TimeSubscription>(),\n                TIME_CONFIG_ID.into(),\n                1,\n            )\n            .map(|update| {\n                if !update.errors.is_empty() {\n                    log::info!(\n                        \"errors loading time config {:?}: {:?}\",\n                        update.keys,\n                        update.errors\n                    );\n                }\n                Message::TimeConfigChange(update.config)\n            }),\n            Subscription::run_with(TypeId::of::<WatcherSubscription>(), |_| {\n                stream::channel(\n                    100,\n                    |mut output: futures::channel::mpsc::Sender<Message>| async move {\n                        let watcher_res = {\n                            let mut output = output.clone();\n                            new_debouncer(\n                                time::Duration::from_millis(250),\n                                Some(time::Duration::from_millis(250)),\n                                move |events_res: notify_debouncer_full::DebounceEventResult| {\n                                    match events_res {\n                                        Ok(mut events) => {\n                                            log::debug!(\"{events:?}\");\n\n                                            events.retain(|event| {\n                                            match &event.kind {\n                                                notify::EventKind::Access(_) => {\n                                                    // Data not mutated\n                                                    false\n                                                }\n                                                notify::EventKind::Modify(\n                                                    notify::event::ModifyKind::Metadata(e),\n                                                ) if (*e != notify::event::MetadataKind::Any\n                                                    && *e\n                                                        != notify::event::MetadataKind::WriteTime) =>\n                                                {\n                                                    // Data not mutated nor modify time changed\n                                                    false\n                                                }\n                                                _ => true\n                                            }\n                                        });\n\n                                            if !events.is_empty() {\n                                                match futures::executor::block_on(async {\n                                                    output.send(Message::NotifyEvents(events)).await\n                                                }) {\n                                                    Ok(()) => {}\n                                                    Err(err) => {\n                                                        log::warn!(\n                                                            \"failed to send notify events: {err:?}\"\n                                                        );\n                                                    }\n                                                }\n                                            }\n                                        }\n                                        Err(err) => {\n                                            log::warn!(\"failed to watch files: {err:?}\");\n                                        }\n                                    }\n                                },\n                            )\n                        };\n\n                        match watcher_res {\n                            Ok(watcher) => {\n                                match output\n                                    .send(Message::NotifyWatcher(WatcherWrapper {\n                                        watcher_opt: Some(watcher),\n                                    }))\n                                    .await\n                                {\n                                    Ok(()) => {}\n                                    Err(err) => {\n                                        log::warn!(\"failed to send notify watcher: {err:?}\");\n                                    }\n                                }\n                            }\n                            Err(err) => {\n                                log::warn!(\"failed to create file watcher: {err:?}\");\n                            }\n                        }\n\n                        std::future::pending().await\n                    },\n                )\n            }),\n            Subscription::run_with(TypeId::of::<TrashWatcherSubscription>(), |_| {\n                stream::channel(\n                    1,\n                    |mut output: futures::channel::mpsc::Sender<Message>| async move {\n                        let watcher_res = new_debouncer(\n                            time::Duration::from_millis(250),\n                            Some(time::Duration::from_millis(250)),\n                            move |event_res: notify_debouncer_full::DebounceEventResult| {\n                                match event_res {\n                                    Ok(events) => {\n                                        // Rescan on any event. We don't need to evaluate each event\n                                        // because as long as the trash changed in any way we need to\n                                        // rescan.\n                                        let should_rescan =\n                                            events.iter().any(|event| !event.kind.is_access());\n\n                                        if should_rescan\n                                            && let Err(e) = futures::executor::block_on(async {\n                                                output.send(Message::RescanTrash).await\n                                            })\n                                        {\n                                            log::warn!(\n                                                \"trash needs to be rescanned but sending message failed: {e:?}\"\n                                            );\n                                        }\n                                    }\n                                    Err(e) => {\n                                        log::warn!(\"failed to watch trash bin for changes: {e:?}\");\n                                    }\n                                }\n                            },\n                        );\n\n                        match (watcher_res, Trash::folders()) {\n                            (Ok(mut watcher), Ok(trash_bins)) => {\n                                // Watch the \"bins\" themselves as well as the files folder where\n                                // trashed items are placed. This allows us to avoid recursively\n                                // watching the trash which is slow but also properly get events.\n                                let trash_paths = trash_bins\n                                    .into_iter()\n                                    .flat_map(|path| [path.join(\"files\"), path]);\n                                for path in trash_paths {\n                                    if let Err(e) =\n                                        watcher.watch(&path, notify::RecursiveMode::NonRecursive)\n                                    {\n                                        log::warn!(\n                                            \"failed to add trash bin `{}` to watcher: {e:?}\",\n                                            path.display()\n                                        );\n                                    }\n                                }\n\n                                // Don't drop the watcher\n                                std::future::pending().await\n                            }\n                            (Err(e), _) => {\n                                log::warn!(\"failed to create new watcher for trash bin: {e:?}\");\n                            }\n                            (_, Err(e)) => {\n                                log::warn!(\"could not find any valid trash bins to watch: {e:?}\");\n                            }\n                        }\n\n                        std::future::pending().await\n                    },\n                )\n            }),\n            #[cfg(all(\n                not(feature = \"desktop-applet\"),\n                not(target_os = \"ios\"),\n                not(target_os = \"android\")\n            ))]\n            Subscription::run_with(TypeId::of::<RecentsWatcherSubscription>(), |_| {\n                stream::channel(\n                    1,\n                    |mut output: futures::channel::mpsc::Sender<Message>| async move {\n                        let Some(recents_path) = recently_used_xbel::dir() else {\n                            log::warn!(\n                                \"failed to watch recents changes: .recently_used.xbel does not exist\"\n                            );\n                            return std::future::pending().await;\n                        };\n\n                        let watcher_res = new_debouncer(\n                            time::Duration::from_millis(250),\n                            Some(time::Duration::from_millis(250)),\n                            move |event_res: notify_debouncer_full::DebounceEventResult| {\n                                match event_res {\n                                    Ok(events) => {\n                                        // Programs differ in how they modify the recents file so the\n                                        // rescan is triggered on any event but access.\n                                        if events.iter().any(|event| {\n                                            let kind = event.kind;\n                                            kind.is_create()\n                                                || kind.is_modify()\n                                                || kind.is_remove()\n                                                || kind.is_other()\n                                        }) && let Err(e) = futures::executor::block_on(async {\n                                            output.send(Message::RescanRecents).await\n                                        }) {\n                                            log::warn!(\n                                                \"open recents tabs need to be updated but sending message failed: {e:?}\"\n                                            );\n                                        }\n                                    }\n                                    Err(e) => {\n                                        log::warn!(\n                                            \"failed to watch recents file for changes: {e:?}\"\n                                        )\n                                    }\n                                }\n                            },\n                        );\n\n                        match watcher_res {\n                            Ok(mut watcher) => {\n                                if let Err(e) = watcher\n                                    .watch(&recents_path, notify::RecursiveMode::NonRecursive)\n                                {\n                                    log::warn!(\n                                        \"failed to add recents file `{}` to watcher: {}\",\n                                        recents_path.display(),\n                                        e\n                                    );\n                                }\n\n                                // Don't drop the watcher.\n                                std::future::pending::<()>().await;\n                            }\n                            Err(e) => {\n                                log::warn!(\"failed to create new watcher for recents file: {e:?}\")\n                            }\n                        }\n\n                        std::future::pending().await\n                    },\n                )\n            }),\n        ];\n\n        if let Some(scroll_speed) = self.auto_scroll_speed {\n            subscriptions.push(\n                iced::time::every(time::Duration::from_millis(10))\n                    .with(scroll_speed)\n                    .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)),\n            );\n        }\n\n        subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {\n            mounter\n                .subscription()\n                .with(*key)\n                .map(|(key, mounter_message)| match mounter_message {\n                    MounterMessage::Items(items) => Message::MounterItems(key, items),\n                    MounterMessage::MountResult(item, res) => Message::MountResult(key, item, res),\n                    MounterMessage::NetworkAuth(uri, auth, auth_tx) => {\n                        Message::NetworkAuth(key, uri, auth, auth_tx)\n                    }\n                    MounterMessage::NetworkResult(uri, res) => {\n                        Message::NetworkResult(key, uri, res)\n                    }\n                })\n        }));\n\n        if !self.pending_operations.is_empty() {\n            //TODO: inhibit suspend/shutdown?\n\n            if self.core.main_window_id().is_some() {\n                // Force refresh the UI every 100ms while an operation is active.\n                if self\n                    .pending_operations\n                    .values()\n                    .any(|(_, controller)| !controller.is_paused())\n                {\n                    subscriptions.push(\n                        cosmic::iced::time::every(Duration::from_millis(100))\n                            .map(|_| Message::None),\n                    );\n                }\n            } else {\n                // Handle notification when window is closed and operations are in progress\n                #[cfg(feature = \"notify\")]\n                {\n                    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n                    struct NotificationSubscription;\n                    subscriptions.push(Subscription::run_with(\n                        TypeId::of::<NotificationSubscription>(),\n                        |_| {\n                            stream::channel(\n                                1,\n                                move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move {\n                                    tokio::task::spawn_blocking(move || {\n                                        match notify_rust::Notification::new()\n                                            .summary(&fl!(\"notification-in-progress\"))\n                                            .timeout(notify_rust::Timeout::Never)\n                                            .show()\n                                        {\n                                            Ok(notification) => {\n                                                let _ = futures::executor::block_on(async {\n                                                    msg_tx\n                                                        .send(Message::Notification(Arc::new(\n                                                            Mutex::new(notification),\n                                                        )))\n                                                        .await\n                                                });\n                                            }\n                                            Err(err) => {\n                                                log::warn!(\"failed to create notification: {err}\");\n                                            }\n                                        }\n                                    })\n                                    .await\n                                    .unwrap();\n\n                                    std::future::pending().await\n                                },\n                            )\n                        },\n                    ));\n                }\n            }\n        }\n\n        let mut selected_previews = Vec::new();\n        match self.mode {\n            Mode::App => {\n                if self.core.window.show_context\n                    && let ContextPage::Preview(entity_opt, PreviewKind::Selected) =\n                        self.context_page\n                {\n                    selected_previews\n                        .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active())));\n                }\n            }\n            Mode::Desktop => {\n                for window_kind in self.windows.values().map(|window| &window.kind) {\n                    if let WindowKind::Preview(entity_opt, _) = window_kind {\n                        selected_previews\n                            .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active())));\n                    }\n                }\n            }\n        }\n\n        subscriptions.extend(self.tab_model.iter().filter_map(|entity| {\n            let tab = self.tab_model.data::<Tab>(entity)?;\n            Some(\n                tab.subscription(\n                    selected_previews\n                        .iter()\n                        .any(|preview| preview.as_ref() == Some(entity).as_ref()),\n                )\n                .with(entity)\n                .map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)),\n            )\n        }));\n\n        Subscription::batch(subscriptions)\n    }\n}\n\n// Utilities to build a temporary file hierarchy for tests.\n//\n// Ideally, tests would use the cap-std crate which limits path traversal.\n#[cfg(test)]\npub(crate) mod test_utils {\n    use std::cmp::Ordering;\n    use std::fs::File;\n    use std::io::{self, Write};\n    use std::iter;\n    use std::path::Path;\n\n    use log::{debug, trace};\n    use tempfile::{TempDir, tempdir};\n\n    use crate::config::{IconSizes, TabConfig, ThumbCfg};\n    use crate::tab::Item;\n\n    use super::*;\n\n    // Default number of files, directories, and nested directories for test file system\n    pub const NUM_FILES: usize = 2;\n    pub const NUM_HIDDEN: usize = 1;\n    pub const NUM_DIRS: usize = 2;\n    pub const NUM_NESTED: usize = 1;\n    pub const NAME_LEN: usize = 5;\n\n    /// Add `n` temporary files in `dir`\n    ///\n    /// Each file is assigned a numeric name from [0, n) with a prefix.\n    pub fn file_flat_hier<D: AsRef<Path>>(dir: D, n: usize, prefix: &str) -> io::Result<Vec<File>> {\n        let dir = dir.as_ref();\n        (0..n)\n            .map(|i| -> io::Result<File> {\n                let name = format!(\"{prefix}{i}\");\n                let path = dir.join(&name);\n\n                let mut file = File::create(path)?;\n                file.write_all(name.as_bytes())?;\n\n                Ok(file)\n            })\n            .collect()\n    }\n\n    // Random alphanumeric String of length `len`\n    fn rand_string(len: usize) -> String {\n        let mut rng = fastrand::Rng::new();\n        iter::repeat_with(|| rng.alphanumeric()).take(len).collect()\n    }\n\n    /// Create a small, temporary file hierarchy.\n    ///\n    /// # Arguments\n    ///\n    /// * `files` - Number of files to create in temp directories\n    /// * `hidden` - Number of hidden files to create\n    /// * `dirs` - Number of directories to create\n    /// * `nested` - Number of nested directories to create in new dirs\n    /// * `name_len` - Length of randomized directory names\n    pub fn simple_fs(\n        files: usize,\n        hidden: usize,\n        dirs: usize,\n        nested: usize,\n        name_len: usize,\n    ) -> io::Result<TempDir> {\n        // Files created inside of a TempDir are deleted with the directory\n        // TempDir won't leak resources as long as the destructor runs\n        let root = tempdir()?;\n        debug!(\"Root temp directory: {}\", root.as_ref().display());\n        trace!(\n            \"Creating {files} files and {hidden} hidden files in {dirs} temp dirs with {nested} nested temp dirs\"\n        );\n\n        // All paths for directories and nested directories\n        let paths = iter::repeat_with(|| {\n            let root = root.as_ref();\n            let current = rand_string(name_len);\n\n            iter::once(root.join(&current)).chain(\n                iter::repeat_with(move || {\n                    let mut path = root.join(&current);\n                    path.push(rand_string(name_len));\n                    path\n                })\n                .take(nested),\n            )\n        })\n        .take(dirs)\n        .flatten();\n\n        // Create directories from `paths` and add a few files\n        for path in paths {\n            fs::create_dir_all(&path)?;\n\n            // Normal files\n            file_flat_hier(&path, files, \"\")?;\n            // Hidden files\n            file_flat_hier(&path, hidden, \".\")?;\n\n            for entry in path.read_dir()? {\n                let entry = entry?;\n                if entry.file_type()?.is_file() {\n                    trace!(\"Created file: {}\", entry.path().display());\n                }\n            }\n        }\n\n        Ok(root)\n    }\n\n    /// Empty file hierarchy\n    pub fn empty_fs() -> io::Result<TempDir> {\n        tempdir()\n    }\n\n    /// Sort files.\n    ///\n    /// Directories are placed before files.\n    /// Files are lexically sorted.\n    /// This is more or less copied right from the [Tab] code\n    pub fn sort_files(a: &Path, b: &Path) -> Ordering {\n        match (a.is_dir(), b.is_dir()) {\n            (true, false) => Ordering::Less,\n            (false, true) => Ordering::Greater,\n            _ => LANGUAGE_SORTER.compare(\n                a.file_name()\n                    .expect(\"temp entries should have names\")\n                    .to_str()\n                    .expect(\"temp entries should be valid UTF-8\"),\n                b.file_name()\n                    .expect(\"temp entries should have names\")\n                    .to_str()\n                    .expect(\"temp entries should be valid UTF-8\"),\n            ),\n        }\n    }\n\n    /// Read directory entries from `path` and sort.\n    pub fn read_dir_sorted(path: &Path) -> io::Result<Vec<PathBuf>> {\n        let mut entries: Vec<_> = path\n            .read_dir()?\n            .map(|maybe_entry| maybe_entry.map(|entry| entry.path()))\n            .collect::<io::Result<_>>()?;\n        entries.sort_by(|a, b| sort_files(a, b));\n\n        Ok(entries)\n    }\n\n    /// Filter `path` for directories\n    pub fn filter_dirs(path: &Path) -> io::Result<impl Iterator<Item = PathBuf> + use<>> {\n        Ok(path.read_dir()?.filter_map(|entry| {\n            entry.ok().and_then(|entry| {\n                let path = entry.path();\n                path.is_dir().then_some(path)\n            })\n        }))\n    }\n\n    // Filter `path` for files\n    pub fn filter_files(path: &Path) -> io::Result<impl Iterator<Item = PathBuf> + use<>> {\n        Ok(path.read_dir()?.filter_map(|entry| {\n            entry.ok().and_then(|entry| {\n                let path = entry.path();\n                path.is_file().then_some(path)\n            })\n        }))\n    }\n\n    /// Boiler plate for Tab tests\n    pub fn tab_click_new(\n        files: usize,\n        hidden: usize,\n        dirs: usize,\n        nested: usize,\n        name_len: usize,\n    ) -> io::Result<(TempDir, Tab)> {\n        let fs = simple_fs(files, hidden, dirs, nested, name_len)?;\n        let path = fs.path();\n\n        // New tab with items\n        let location = Location::Path(path.to_owned());\n        let (parent_item_opt, items) = location.scan(IconSizes::default());\n        let mut tab = Tab::new(\n            location,\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n        tab.parent_item_opt = parent_item_opt;\n        tab.set_items(items);\n\n        // Ensure correct number of directories as a sanity check\n        let items = tab.items_opt().expect(\"tab should be populated with Items\");\n        assert_eq!(NUM_DIRS, items.len());\n\n        Ok((fs, tab))\n    }\n\n    /// Equality for [Path] and [Item].\n    pub fn eq_path_item(path: &Path, item: &Item) -> bool {\n        let name = path\n            .file_name()\n            .expect(\"temp entries should have names\")\n            .to_str()\n            .expect(\"temp entries should be valid UTF-8\");\n        let is_dir = path.is_dir();\n\n        // NOTE: I don't want to change `tab::hidden_attribute` to `pub(crate)` for\n        // tests without asking\n        #[cfg(not(target_os = \"windows\"))]\n        let is_hidden = name.starts_with('.');\n\n        #[cfg(target_os = \"windows\")]\n        let is_hidden = {\n            use std::os::windows::fs::MetadataExt;\n            const FILE_ATTRIBUTE_HIDDEN: u32 = 2;\n            let metadata = path.metadata().expect(\"fetching file metadata\");\n            metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN\n        };\n\n        name == item.name\n            && is_dir == item.metadata.is_dir()\n            && path == item.path_opt().expect(\"item should have path\")\n            && is_hidden == item.hidden\n    }\n\n    /// Asserts `tab`'s location changed to `path`\n    pub fn assert_eq_tab_path(tab: &Tab, path: &Path) {\n        // Paths should be the same\n        let Some(tab_path) = tab.location.path_opt() else {\n            panic!(\"Expected tab's location to be a path\");\n        };\n\n        assert_eq!(\n            path,\n            tab_path,\n            \"Tab's path is {} instead of being updated to {}\",\n            tab_path.display(),\n            path.display()\n        );\n    }\n\n    /// Assert that tab's items are equal to a path's entries.\n    pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) {\n        let Some(tab_path) = tab.location.path_opt() else {\n            panic!(\"Expected tab's location to be a path\");\n        };\n\n        // Tab items are sorted so paths from read_dir must be too\n        let entries = read_dir_sorted(path).expect(\"should be able to read paths from temp dir\");\n\n        // Check lengths.\n        // `items_opt` is optional and the directory at `path` may have zero entries\n        // Therefore, this doesn't panic if `items_opt` is None\n        let items_len = tab.items_opt().map(Vec::len).unwrap_or_default();\n        assert_eq!(entries.len(), items_len);\n\n        assert!(\n            entries\n                .into_iter()\n                .zip(tab.items_opt().map_or([].as_slice(), Vec::as_slice))\n                .all(|(a, b)| eq_path_item(&a, b)),\n            \"Path ({}) and Tab path ({}) don't have equal contents\",\n            path.display(),\n            tab_path.display()\n        );\n    }\n}\n"
  },
  {
    "path": "src/archive.rs",
    "content": "use crate::mime_icon::mime_for_path;\nuse crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk};\nuse cosmic::iced::futures;\nuse jiff::Zoned;\nuse jiff::civil::DateTime;\nuse jiff::tz::TimeZone;\nuse std::collections::HashSet;\nuse std::fs;\nuse std::io::{self, Read, Write};\nuse std::path::{Path, PathBuf};\nuse std::time::SystemTime;\nuse zip::result::ZipError;\n\npub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[\n    \"application/gzip\",\n    \"application/x-compressed-tar\",\n    \"application/x-tar\",\n    \"application/zip\",\n    #[cfg(feature = \"bzip2\")]\n    \"application/x-bzip\",\n    #[cfg(feature = \"bzip2\")]\n    \"application/x-bzip-compressed-tar\",\n    #[cfg(feature = \"bzip2\")]\n    \"application/x-bzip2\",\n    #[cfg(feature = \"bzip2\")]\n    \"application/x-bzip2-compressed-tar\",\n    #[cfg(feature = \"lzma-rust2\")]\n    \"application/x-xz\",\n    #[cfg(feature = \"lzma-rust2\")]\n    \"application/x-xz-compressed-tar\",\n];\n\npub const SUPPORTED_EXTENSIONS: &[&str] = &[\n    \".tar.bz2\",\n    \".tar.gz\",\n    \".tar.lzma\",\n    \".tar.xz\",\n    \".tgz\",\n    \".tar\",\n    \".zip\",\n];\n\npub fn extract(\n    path: &Path,\n    new_dir: &Path,\n    password: &Option<String>,\n    controller: &Controller,\n) -> Result<(), OperationError> {\n    let mime = mime_for_path(path, None, false);\n    let password = password.as_deref();\n    match mime.essence_str() {\n        \"application/gzip\" | \"application/x-compressed-tar\" => {\n            OpReader::new(path, controller.clone())\n                .map(io::BufReader::new)\n                .map(flate2::read::GzDecoder::new)\n                .map(tar::Archive::new)\n                .and_then(|mut archive| archive.unpack(new_dir))\n                .map_err(|e| OperationError::from_err(e, controller))?;\n        }\n        \"application/x-tar\" => OpReader::new(path, controller.clone())\n            .map(io::BufReader::new)\n            .map(tar::Archive::new)\n            .and_then(|mut archive| archive.unpack(new_dir))\n            .map_err(|e| OperationError::from_err(e, controller))?,\n        \"application/zip\" => fs::File::open(path)\n            .map(io::BufReader::new)\n            .map(zip::ZipArchive::new)\n            .map_err(|e| OperationError::from_err(e, controller))?\n            .and_then(move |mut archive| {\n                zip_extract(&mut archive, new_dir, password, controller.clone())\n            })\n            .map_err(|e| match e {\n                ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)\n                | ZipError::InvalidPassword => {\n                    OperationError::from_kind(OperationErrorType::PasswordRequired, controller)\n                }\n                _ => OperationError::from_err(e, controller),\n            })?,\n        #[cfg(feature = \"bzip2\")]\n        \"application/x-bzip\"\n        | \"application/x-bzip-compressed-tar\"\n        | \"application/x-bzip2\"\n        | \"application/x-bzip2-compressed-tar\" => OpReader::new(path, controller.clone())\n            .map(io::BufReader::new)\n            .map(bzip2::read::BzDecoder::new)\n            .map(tar::Archive::new)\n            .and_then(|mut archive| archive.unpack(new_dir))\n            .map_err(|e| OperationError::from_err(e, controller))?,\n        #[cfg(feature = \"lzma-rust2\")]\n        \"application/x-xz\" | \"application/x-xz-compressed-tar\" => {\n            OpReader::new(path, controller.clone())\n                .map(io::BufReader::new)\n                .map(|reader| lzma_rust2::XzReader::new(reader, true))\n                .map(tar::Archive::new)\n                .and_then(|mut archive| archive.unpack(new_dir))\n                .map_err(|e| OperationError::from_err(e, controller))?;\n        }\n        _ => Err(OperationError::from_err(\n            format!(\"unsupported mime type {mime:?}\"),\n            controller,\n        ))?,\n    }\n    Ok(())\n}\n\n// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added\nfn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(\n    archive: &mut zip::ZipArchive<R>,\n    directory: P,\n    password: Option<&str>,\n    controller: Controller,\n) -> zip::result::ZipResult<()> {\n    use std::ffi::OsString;\n    use std::fs;\n    use zip::result::ZipError;\n\n    fn make_writable_dir_all<T: AsRef<Path>>(\n        outpath: T,\n        target_dirs: &mut HashSet<PathBuf>,\n    ) -> Result<(), ZipError> {\n        let path = outpath.as_ref();\n        if !path.exists() {\n            fs::create_dir_all(path)?;\n        }\n        if !target_dirs.contains(path) {\n            target_dirs.insert(path.to_path_buf());\n        }\n\n        #[cfg(unix)]\n        {\n            // Dirs must be writable until all normal files are extracted\n            use std::os::unix::fs::PermissionsExt;\n            fs::set_permissions(\n                path,\n                fs::Permissions::from_mode(0o700 | fs::metadata(path)?.permissions().mode()),\n            )?;\n        }\n        Ok(())\n    }\n\n    let mut buffer = vec![0; 4 * 1024 * 1024];\n    let total_files = archive.len();\n    let mut written_files = Vec::with_capacity(total_files);\n    let mut target_dirs = HashSet::new();\n    #[cfg(unix)]\n    let mut files_by_unix_mode = Vec::with_capacity(total_files);\n    let mut files_by_last_modified = Vec::with_capacity(total_files);\n\n    for i in 0..total_files {\n        futures::executor::block_on(async {\n            controller\n                .check()\n                .await\n                .map_err(|s| io::Error::other(OperationError::from_state(s, &controller)))\n        })?;\n\n        controller.set_progress(i as f32 / total_files as f32);\n\n        let mut file = match password {\n            None => archive.by_index(i),\n            Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),\n        }?;\n\n        let filepath = file\n            .enclosed_name()\n            .ok_or(ZipError::InvalidArchive(\"Invalid file path\".into()))?;\n\n        let outpath = directory.as_ref().join(filepath);\n\n        if let Some(last_modified) = file.last_modified() {\n            files_by_last_modified.push((outpath.clone(), last_modified));\n        }\n\n        if file.is_dir() {\n            make_writable_dir_all(&outpath, &mut target_dirs)?;\n\n            #[cfg(unix)]\n            if let Some(mode) = file.unix_mode() {\n                files_by_unix_mode.push((outpath, mode));\n            }\n            continue;\n        }\n\n        if let Some(parent) = outpath.parent() {\n            make_writable_dir_all(parent, &mut target_dirs)?;\n        }\n\n        if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {\n            let mut target = Vec::with_capacity(file.size() as usize);\n            file.read_to_end(&mut target)?;\n            // File no longer needed, drop to allow reading target on windows\n            drop(file);\n\n            #[cfg(unix)]\n            {\n                use std::os::unix::ffi::OsStringExt;\n                let target = OsString::from_vec(target);\n                std::os::unix::fs::symlink(&target, outpath.as_path())?;\n            }\n            #[cfg(windows)]\n            {\n                let Ok(target) = String::from_utf8(target) else {\n                    return Err(ZipError::InvalidArchive(\n                        \"Invalid UTF-8 as symlink target\".into(),\n                    ));\n                };\n                let target_is_dir_from_archive = match password {\n                    None => archive.by_name(&target),\n                    Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()),\n                }\n                .map_or(false, |x| x.is_dir());\n                let target_path = directory.as_ref().join(OsString::from(target.to_string()));\n                let target_is_dir = if target_is_dir_from_archive {\n                    true\n                } else if let Ok(meta) = std::fs::metadata(&target_path) {\n                    meta.is_dir()\n                } else {\n                    false\n                };\n                if target_is_dir {\n                    std::os::windows::fs::symlink_dir(target_path, outpath.as_path())?;\n                } else {\n                    std::os::windows::fs::symlink_file(target_path, outpath.as_path())?;\n                }\n            }\n\n            written_files.push(outpath);\n            continue;\n        }\n\n        let total = file.size();\n        let mut outfile = fs::File::create(&outpath)?;\n        let mut current = 0;\n        loop {\n            futures::executor::block_on(async {\n                controller\n                    .check()\n                    .await\n                    .map_err(|s| io::Error::other(OperationError::from_state(s, &controller)))\n            })?;\n\n            let count = file.read(&mut buffer)?;\n            if count == 0 {\n                break;\n            }\n            outfile.write_all(&buffer[..count])?;\n            current += count as u64;\n\n            if current < total {\n                let file_progress = current as f32 / total as f32;\n                let total_progress = (i as f32 + file_progress) / total_files as f32;\n                controller.set_progress(total_progress);\n            }\n        }\n\n        // Check for real permissions, which we'll set in a second pass\n        #[cfg(unix)]\n        if let Some(mode) = file.unix_mode() {\n            files_by_unix_mode.push((outpath.clone(), mode));\n        }\n\n        written_files.push(outpath);\n    }\n    #[cfg(unix)]\n    {\n        use std::cmp::Reverse;\n        use std::os::unix::fs::PermissionsExt;\n\n        if files_by_unix_mode.len() > 1 {\n            // Ensure we update children's permissions before making a parent unwritable\n            files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.components().count()));\n        }\n        for (path, mode) in files_by_unix_mode {\n            fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;\n        }\n    }\n\n    for (path, last_modified) in files_by_last_modified {\n        if let Some(modified) = zip_date_time_to_system_time(last_modified) {\n            let file_time = filetime::FileTime::from_system_time(modified);\n            filetime::set_file_mtime(&path, file_time)?;\n        }\n    }\n\n    // Flush files to disk\n    futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await });\n\n    Ok(())\n}\n\nfn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> {\n    let dt = DateTime::new(\n        date_time.year() as i16,\n        date_time.month() as i8,\n        date_time.day() as i8,\n        date_time.hour() as i8,\n        date_time.minute() as i8,\n        date_time.second() as i8,\n        0,\n    )\n    .ok()?;\n    TimeZone::system()\n        .to_ambiguous_zoned(dt)\n        .later()\n        .ok()\n        .map(SystemTime::from)\n}\n\npub fn system_time_to_zip_date_time(system_time: SystemTime) -> Option<zip::DateTime> {\n    let date_time = Zoned::try_from(system_time).ok()?;\n\n    zip::DateTime::from_date_and_time(\n        date_time.year() as u16,\n        date_time.month() as u8,\n        date_time.day() as u8,\n        date_time.hour() as u8,\n        date_time.minute() as u8,\n        date_time.second() as u8,\n    )\n    .ok()\n}\n"
  },
  {
    "path": "src/channel.rs",
    "content": "// Copyright 2025 System76 <info@system76.com>\n// SPDX-License-Identifier: MPL-2.0\n\nuse std::collections::VecDeque;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\n\n/// Create a channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.\npub fn channel<Message>() -> (Sender<Message>, Receiver<Message>) {\n    let channel = Arc::new(Channel {\n        queue: Mutex::new(VecDeque::default()),\n        notify: tokio::sync::Notify::const_new(),\n        closed: AtomicBool::new(false),\n    });\n\n    (Sender(channel.clone()), Receiver(channel))\n}\n\n/// A channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.\nstruct Channel<Message> {\n    pub(self) queue: Mutex<VecDeque<Message>>,\n    /// Set when a new message has been stored.\n    pub(self) notify: tokio::sync::Notify,\n    /// Set when the receiver is dropped.\n    pub(self) closed: AtomicBool,\n}\n\npub struct Sender<Message>(Arc<Channel<Message>>);\n\nimpl<Message> Sender<Message> {\n    pub fn send(&self, message: Message) {\n        self.0.queue.lock().unwrap().push_back(message);\n        self.0.notify.notify_one();\n    }\n}\n\nimpl<Message> Drop for Sender<Message> {\n    fn drop(&mut self) {\n        self.0.closed.store(true, Ordering::SeqCst);\n        self.0.notify.notify_one();\n    }\n}\n\npub struct Receiver<Message>(Arc<Channel<Message>>);\n\nimpl<Message> Receiver<Message> {\n    /// Returns a value until the sender is dropped.\n    pub async fn recv(&self) -> Option<Message> {\n        loop {\n            {\n                let mut queue = self.0.queue.lock().unwrap();\n                if let Some(value) = queue.pop_front() {\n                    if queue.capacity() - queue.len() > 32 {\n                        let capacity = queue.len().next_power_of_two();\n                        queue.shrink_to(capacity);\n                    }\n                    drop(queue);\n                    return Some(value);\n                }\n            }\n\n            if self.0.closed.load(Ordering::SeqCst) {\n                return None;\n            }\n\n            self.0.notify.notified().await;\n        }\n    }\n\n    pub fn try_recv(&self) -> Option<Message> {\n        self.0.queue.lock().unwrap().pop_front()\n    }\n}\n"
  },
  {
    "path": "src/clipboard.rs",
    "content": "// Copyright 2024 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};\nuse std::borrow::Cow;\nuse std::error::Error;\nuse std::path::{Path, PathBuf};\nuse std::str;\nuse url::Url;\n\n#[derive(Clone, Copy, Debug)]\npub enum ClipboardKind {\n    Copy,\n    Cut { is_dnd: bool },\n}\n\n#[derive(Clone, Debug)]\npub struct ClipboardCopy {\n    pub available: Cow<'static, [String]>,\n    pub text_plain: Cow<'static, [u8]>,\n    pub text_uri_list: Cow<'static, [u8]>,\n    pub x_special_gnome_copied_files: Cow<'static, [u8]>,\n}\n\nimpl ClipboardCopy {\n    pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: impl IntoIterator<Item = P>) -> Self {\n        let available = vec![\n            \"text/plain\".to_string(),\n            \"text/plain;charset=utf-8\".to_string(),\n            \"UTF8_STRING\".to_string(),\n            \"text/uri-list\".to_string(),\n            \"x-special/gnome-copied-files\".to_string(),\n        ];\n        let mut text_plain = String::new();\n        let mut text_uri_list = String::new();\n        let mut x_special_gnome_copied_files = match kind {\n            ClipboardKind::Copy => \"copy\",\n            ClipboardKind::Cut { .. } => \"cut\",\n        }\n        .to_string();\n        //TODO: do we have to use \\r\\n?\n        let cr_nl = \"\\r\\n\";\n        for path in paths {\n            let path = path.as_ref();\n\n            match path.to_str() {\n                Some(path_str) => {\n                    if !text_plain.is_empty() {\n                        text_plain.push_str(cr_nl);\n                    }\n                    //TODO: what if the path contains CR or NL?\n                    text_plain.push_str(path_str);\n                }\n                None => {\n                    //TODO: allow non-UTF-8?\n                    log::warn!(\n                        \"{} is not valid UTF-8, not adding to text/plain clipboard\",\n                        path.display()\n                    );\n                }\n            }\n\n            match Url::from_file_path(path) {\n                Ok(url) => {\n                    let url_str = url.as_ref();\n\n                    text_uri_list.push_str(url_str);\n                    text_uri_list.push_str(cr_nl);\n\n                    x_special_gnome_copied_files.push('\\n');\n                    x_special_gnome_copied_files.push_str(url_str);\n                }\n                Err(()) => {\n                    log::warn!(\n                        \"{} cannot be turned into a URL, not adding to text/uri-list clipboard\",\n                        path.display()\n                    );\n                }\n            }\n        }\n        Self {\n            available: Cow::from(available),\n            text_plain: Cow::from(text_plain.into_bytes()),\n            text_uri_list: Cow::from(text_uri_list.into_bytes()),\n            x_special_gnome_copied_files: Cow::from(x_special_gnome_copied_files.into_bytes()),\n        }\n    }\n}\n\nimpl AsMimeTypes for ClipboardCopy {\n    fn available(&self) -> Cow<'static, [String]> {\n        self.available.clone()\n    }\n\n    fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {\n        match mime_type {\n            \"text/plain\" | \"text/plain;charset=utf-8\" | \"UTF8_STRING\" => {\n                Some(self.text_plain.clone())\n            }\n            \"text/uri-list\" => Some(self.text_uri_list.clone()),\n            \"x-special/gnome-copied-files\" => Some(self.x_special_gnome_copied_files.clone()),\n            _ => None,\n        }\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct ClipboardPaste {\n    pub kind: ClipboardKind,\n    pub paths: Vec<PathBuf>,\n}\n\nimpl AllowedMimeTypes for ClipboardPaste {\n    fn allowed() -> Cow<'static, [String]> {\n        Cow::from(vec![\n            \"x-special/gnome-copied-files\".to_string(),\n            \"text/uri-list\".to_string(),\n        ])\n    }\n}\n\nimpl TryFrom<(Vec<u8>, String)> for ClipboardPaste {\n    type Error = Box<dyn Error>;\n    fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {\n        let (data, mime) = value;\n        // Assume the kind is Copy if not provided by the mime type\n        let mut kind = ClipboardKind::Copy;\n        let mut paths = Vec::new();\n\n        match mime.as_str() {\n            \"text/uri-list\" => {\n                let text = str::from_utf8(&data)?;\n                let _lines = text.lines();\n\n                for line in text.lines() {\n                    let url = Url::parse(line)?;\n                    match url.to_file_path() {\n                        Ok(path) => paths.push(path),\n                        Err(()) => Err(format!(\"invalid file URL {url:?}\"))?,\n                    }\n                }\n            }\n            \"x-special/gnome-copied-files\" => {\n                let text = str::from_utf8(&data)?;\n\n                for (i, line) in text.lines().enumerate() {\n                    if i == 0 {\n                        kind = match line {\n                            \"copy\" => ClipboardKind::Copy,\n                            \"cut\" => ClipboardKind::Cut { is_dnd: false },\n                            _ => Err(format!(\"unsupported clipboard operation {line:?}\"))?,\n                        };\n                    } else {\n                        let url = Url::parse(line)?;\n                        match url.to_file_path() {\n                            Ok(path) => paths.push(path),\n                            Err(()) => Err(format!(\"invalid file URL {url:?}\"))?,\n                        }\n                    }\n                }\n            }\n            _ => Err(format!(\"unsupported mime type {mime:?}\"))?,\n        }\n        Ok(Self { kind, paths })\n    }\n}\n\n/// Image data from clipboard for pasting as a new file.\n#[derive(Clone, Debug)]\npub struct ClipboardPasteImage {\n    pub data: Vec<u8>,\n    pub mime_type: String,\n}\n\nimpl AllowedMimeTypes for ClipboardPasteImage {\n    fn allowed() -> Cow<'static, [String]> {\n        Cow::from(vec![\n            \"image/png\".to_string(),\n            \"image/jpeg\".to_string(),\n            \"image/gif\".to_string(),\n            \"image/bmp\".to_string(),\n            \"image/webp\".to_string(),\n            \"image/tiff\".to_string(),\n            \"image/x-tiff\".to_string(),\n            \"image/svg+xml\".to_string(),\n            \"image/x-icon\".to_string(),\n            \"image/vnd.microsoft.icon\".to_string(),\n            \"image/x-bmp\".to_string(),\n            \"image/x-ms-bmp\".to_string(),\n            \"image/pjpeg\".to_string(),\n            \"image/x-png\".to_string(),\n            \"image/avif\".to_string(),\n            \"image/heic\".to_string(),\n            \"image/heif\".to_string(),\n            \"image/jxl\".to_string(),\n        ])\n    }\n}\n\nimpl TryFrom<(Vec<u8>, String)> for ClipboardPasteImage {\n    type Error = Box<dyn Error>;\n    fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {\n        let (data, mime) = value;\n        if data.is_empty() {\n            return Err(\"Empty image data\".into());\n        }\n        Ok(Self {\n            data,\n            mime_type: mime,\n        })\n    }\n}\n\nimpl ClipboardPasteImage {\n    /// Get the file extension for the image based on MIME type.\n    /// Returns None if the MIME type is not recognized.\n    pub fn extension(&self) -> Option<&'static str> {\n        match self.mime_type.as_str() {\n            \"image/png\" | \"image/x-png\" => Some(\"png\"),\n            \"image/jpeg\" | \"image/pjpeg\" => Some(\"jpg\"),\n            \"image/gif\" => Some(\"gif\"),\n            \"image/bmp\" | \"image/x-bmp\" | \"image/x-ms-bmp\" => Some(\"bmp\"),\n            \"image/webp\" => Some(\"webp\"),\n            \"image/tiff\" | \"image/x-tiff\" => Some(\"tiff\"),\n            \"image/svg+xml\" => Some(\"svg\"),\n            \"image/x-icon\" | \"image/vnd.microsoft.icon\" => Some(\"ico\"),\n            \"image/avif\" => Some(\"avif\"),\n            \"image/heic\" => Some(\"heic\"),\n            \"image/heif\" => Some(\"heif\"),\n            \"image/jxl\" => Some(\"jxl\"),\n            _ => None,\n        }\n    }\n}\n\n/// Video data from clipboard for pasting as a new file.\n#[derive(Clone, Debug)]\npub struct ClipboardPasteVideo {\n    pub data: Vec<u8>,\n    pub mime_type: String,\n}\n\nimpl AllowedMimeTypes for ClipboardPasteVideo {\n    fn allowed() -> Cow<'static, [String]> {\n        Cow::from(vec![\n            \"video/mp4\".to_string(),\n            \"video/webm\".to_string(),\n            \"video/ogg\".to_string(),\n            \"video/mpeg\".to_string(),\n            \"video/quicktime\".to_string(),\n            \"video/x-msvideo\".to_string(),\n            \"video/x-matroska\".to_string(),\n            \"video/x-flv\".to_string(),\n            \"video/3gpp\".to_string(),\n            \"video/3gpp2\".to_string(),\n            \"video/x-ms-wmv\".to_string(),\n            \"video/avi\".to_string(),\n        ])\n    }\n}\n\nimpl TryFrom<(Vec<u8>, String)> for ClipboardPasteVideo {\n    type Error = Box<dyn Error>;\n    fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {\n        let (data, mime) = value;\n        if data.is_empty() {\n            return Err(\"Empty video data\".into());\n        }\n        Ok(Self {\n            data,\n            mime_type: mime,\n        })\n    }\n}\n\nimpl ClipboardPasteVideo {\n    /// Get the file extension for the video based on MIME type.\n    /// Returns None if the MIME type is not recognized.\n    pub fn extension(&self) -> Option<&'static str> {\n        match self.mime_type.as_str() {\n            \"video/mp4\" => Some(\"mp4\"),\n            \"video/webm\" => Some(\"webm\"),\n            \"video/ogg\" => Some(\"ogv\"),\n            \"video/mpeg\" => Some(\"mpeg\"),\n            \"video/quicktime\" => Some(\"mov\"),\n            \"video/x-msvideo\" | \"video/avi\" => Some(\"avi\"),\n            \"video/x-matroska\" => Some(\"mkv\"),\n            \"video/x-flv\" => Some(\"flv\"),\n            \"video/3gpp\" => Some(\"3gp\"),\n            \"video/3gpp2\" => Some(\"3g2\"),\n            \"video/x-ms-wmv\" => Some(\"wmv\"),\n            _ => None,\n        }\n    }\n}\n\n/// Text data from clipboard for pasting as a new text file.\n#[derive(Clone, Debug)]\npub struct ClipboardPasteText {\n    pub data: String,\n}\n\nimpl AllowedMimeTypes for ClipboardPasteText {\n    fn allowed() -> Cow<'static, [String]> {\n        Cow::from(vec![\n            \"text/plain\".to_string(),\n            \"text/plain;charset=utf-8\".to_string(),\n            \"UTF8_STRING\".to_string(),\n            \"STRING\".to_string(),\n            \"TEXT\".to_string(),\n        ])\n    }\n}\n\nimpl TryFrom<(Vec<u8>, String)> for ClipboardPasteText {\n    type Error = Box<dyn Error>;\n    fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {\n        let (data, _mime) = value;\n        if data.is_empty() {\n            return Err(\"Empty text data\".into());\n        }\n        // Use lossy conversion to handle clipboard data that may contain\n        // invalid UTF-8 (e.g., Latin-1 encoded special characters from browsers)\n        let text = String::from_utf8_lossy(&data);\n        Ok(Self {\n            data: text.into_owned(),\n        })\n    }\n}\n\n/// Cached clipboard content for paste operations.\n/// This is needed because Wayland restricts clipboard access from popup windows.\n#[derive(Clone, Debug)]\npub enum ClipboardCache {\n    Files(ClipboardPaste),\n    Image(ClipboardPasteImage),\n    Video(ClipboardPasteVideo),\n    Text(ClipboardPasteText),\n    Empty,\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n\nuse std::any::TypeId;\nuse std::num::NonZeroU16;\nuse std::path::PathBuf;\n\nuse cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;\nuse cosmic::cosmic_config::{self, CosmicConfigEntry};\nuse cosmic::iced::Subscription;\nuse cosmic::{Application, theme};\nuse serde::{Deserialize, Serialize};\n\nuse crate::FxOrderMap;\nuse crate::app::App;\nuse crate::tab::{HeadingOptions, Location, View};\n\npub use crate::context_action::{ContextActionPreset, ContextActionSelection};\n\npub const CONFIG_VERSION: u64 = 1;\n\n// Default icon sizes\npub const ICON_SIZE_LIST: u16 = 32;\npub const ICON_SIZE_LIST_CONDENSED: u16 = 48;\npub const ICON_SIZE_GRID: u16 = 64;\n// TODO: 5 is an arbitrary number. Maybe there's a better icon size max\npub const ICON_SCALE_MAX: u16 = 5;\n\nmacro_rules! percent {\n    ($perc:expr, $pixel:ident) => {\n        (($perc.get() as f32 * $pixel as f32) / 100.).clamp(1., ($pixel * ICON_SCALE_MAX) as _)\n    };\n}\n\n#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]\npub enum AppTheme {\n    Dark,\n    Light,\n    System,\n}\n\nimpl AppTheme {\n    pub fn theme(&self) -> theme::Theme {\n        match self {\n            Self::Dark => {\n                let mut t = theme::system_dark();\n                t.theme_type.prefer_dark(Some(true));\n                t\n            }\n            Self::Light => {\n                let mut t = theme::system_light();\n                t.theme_type.prefer_dark(Some(false));\n                t\n            }\n            Self::System => theme::system_preference(),\n        }\n    }\n}\n\n#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]\npub enum Favorite {\n    Home,\n    Documents,\n    Downloads,\n    Music,\n    Pictures,\n    Videos,\n    Path(PathBuf),\n    Network {\n        uri: String,\n        name: String,\n        path: PathBuf,\n    },\n}\n\nimpl Favorite {\n    pub fn from_path(path: PathBuf) -> Self {\n        // Ensure that special folders are handled properly\n        [\n            Self::Home,\n            Self::Documents,\n            Self::Downloads,\n            Self::Music,\n            Self::Pictures,\n            Self::Videos,\n        ]\n        .into_iter()\n        .find(|fav| fav.path_opt().as_ref() == Some(&path))\n        .unwrap_or(Self::Path(path))\n    }\n\n    pub fn path_opt(&self) -> Option<PathBuf> {\n        match self {\n            Self::Home => dirs::home_dir(),\n            Self::Documents => dirs::document_dir(),\n            Self::Downloads => dirs::download_dir(),\n            Self::Music => dirs::audio_dir(),\n            Self::Pictures => dirs::picture_dir(),\n            Self::Videos => dirs::video_dir(),\n            Self::Path(path) => Some(path.clone()),\n            Self::Network { path, .. } => Some(path.clone()),\n        }\n    }\n}\n\n#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]\npub enum TypeToSearch {\n    Recursive,\n    EnterPath,\n    SelectByPrefix,\n}\n\n#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]\n#[serde(default)]\npub struct State {\n    pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>,\n}\n\nimpl Default for State {\n    fn default() -> Self {\n        Self {\n            sort_names: FxOrderMap::from_iter(dirs::download_dir().into_iter().map(|dir| {\n                (\n                    Location::Path(dir).normalize().to_string(),\n                    (HeadingOptions::Modified, false),\n                )\n            })),\n        }\n    }\n}\n\nimpl State {\n    pub fn load() -> (Option<cosmic_config::Config>, Self) {\n        match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) {\n            Ok(config_handler) => {\n                let config = match Self::get_entry(&config_handler) {\n                    Ok(ok) => ok,\n                    Err((errs, config)) => {\n                        log::info!(\"errors loading config: {errs:?}\");\n                        config\n                    }\n                };\n                (Some(config_handler), config)\n            }\n            Err(err) => {\n                log::error!(\"failed to create config handler: {err}\");\n                (None, Self::default())\n            }\n        }\n    }\n\n    pub fn subscription() -> Subscription<cosmic_config::Update<Self>> {\n        struct ConfigSubscription;\n        cosmic_config::config_state_subscription(\n            TypeId::of::<ConfigSubscription>(),\n            App::APP_ID.into(),\n            CONFIG_VERSION,\n        )\n    }\n}\n\n#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]\n#[serde(default)]\npub struct Config {\n    pub app_theme: AppTheme,\n    pub dialog: DialogConfig,\n    pub desktop: DesktopConfig,\n    pub context_actions: Vec<ContextActionPreset>,\n    pub thumb_cfg: ThumbCfg,\n    pub favorites: Vec<Favorite>,\n    pub show_details: bool,\n    pub show_recents: bool,\n    pub tab: TabConfig,\n    pub type_to_search: TypeToSearch,\n}\n\nimpl Config {\n    pub fn load() -> (Option<cosmic_config::Config>, Self) {\n        match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {\n            Ok(config_handler) => {\n                let config = match Self::get_entry(&config_handler) {\n                    Ok(ok) => ok,\n                    Err((errs, config)) => {\n                        log::info!(\"errors loading config: {errs:?}\");\n                        config\n                    }\n                };\n                (Some(config_handler), config)\n            }\n            Err(err) => {\n                log::error!(\"failed to create config handler: {err}\");\n                (None, Self::default())\n            }\n        }\n    }\n\n    pub fn subscription() -> Subscription<cosmic_config::Update<Self>> {\n        struct ConfigSubscription;\n        cosmic_config::config_subscription(\n            TypeId::of::<ConfigSubscription>(),\n            App::APP_ID.into(),\n            CONFIG_VERSION,\n        )\n    }\n\n    /// Construct tab config for dialog\n    pub const fn dialog_tab(&self) -> TabConfig {\n        TabConfig {\n            folders_first: self.dialog.folders_first,\n            icon_sizes: self.dialog.icon_sizes,\n            military_time: self.tab.military_time,\n            show_hidden: self.dialog.show_hidden,\n            single_click: false,\n            view: self.dialog.view,\n        }\n    }\n}\n\nimpl Default for Config {\n    fn default() -> Self {\n        Self {\n            app_theme: AppTheme::System,\n            desktop: DesktopConfig::default(),\n            dialog: DialogConfig::default(),\n            context_actions: Vec::new(),\n            thumb_cfg: ThumbCfg::default(),\n            favorites: vec![\n                Favorite::Home,\n                Favorite::Documents,\n                Favorite::Downloads,\n                Favorite::Music,\n                Favorite::Pictures,\n                Favorite::Videos,\n            ],\n            show_details: false,\n            show_recents: true,\n            tab: TabConfig::default(),\n            type_to_search: TypeToSearch::Recursive,\n        }\n    }\n}\n\n#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]\n#[serde(default)]\npub struct DesktopConfig {\n    pub grid_spacing: NonZeroU16,\n    pub icon_size: NonZeroU16,\n    pub show_content: bool,\n    pub show_mounted_drives: bool,\n    pub show_trash: bool,\n}\n\nimpl Default for DesktopConfig {\n    fn default() -> Self {\n        Self {\n            grid_spacing: 100.try_into().unwrap(),\n            icon_size: 100.try_into().unwrap(),\n            show_content: true,\n            show_mounted_drives: false,\n            show_trash: false,\n        }\n    }\n}\n\nimpl DesktopConfig {\n    pub fn grid_spacing_for(&self, space: u16) -> u16 {\n        percent!(self.grid_spacing, space) as _\n    }\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]\n#[serde(default)]\npub struct DialogConfig {\n    /// Show folders before files\n    pub folders_first: bool,\n    /// Icon zoom\n    pub icon_sizes: IconSizes,\n    /// Show details sidebar\n    pub show_details: bool,\n    /// Show hidden files and folders\n    pub show_hidden: bool,\n    /// Selected view, grid or list\n    pub view: View,\n}\n\nimpl Default for DialogConfig {\n    fn default() -> Self {\n        Self {\n            folders_first: false,\n            icon_sizes: IconSizes::default(),\n            show_details: true,\n            show_hidden: false,\n            view: View::List,\n        }\n    }\n}\n#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]\n#[serde(default)]\npub struct ThumbCfg {\n    pub jobs: NonZeroU16,\n    pub max_mem_mb: NonZeroU16,\n    pub max_size_mb: NonZeroU16,\n}\n\nimpl Default for ThumbCfg {\n    fn default() -> Self {\n        Self {\n            jobs: 4.try_into().unwrap(),\n            max_mem_mb: 2000.try_into().unwrap(),\n            max_size_mb: 64.try_into().unwrap(),\n        }\n    }\n}\n\n/// Global and local [`crate::tab::Tab`] config.\n///\n/// [`TabConfig`] contains options that are passed to each instance of [`crate::tab::Tab`].\n/// These options are set globally through the main config, but each tab may change options\n/// locally. Local changes aren't saved to the main config.\n#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]\n#[serde(default)]\npub struct TabConfig {\n    /// Show folders before files\n    pub folders_first: bool,\n    /// Icon zoom\n    pub icon_sizes: IconSizes,\n    #[serde(skip)]\n    /// 24 hour clock; this is neither serialized nor deserialized because we use the user's global\n    /// preference rather than save it\n    pub military_time: bool,\n    /// Show hidden files and folders\n    pub show_hidden: bool,\n    /// Single click to open\n    pub single_click: bool,\n    /// Selected view, grid or list\n    pub view: View,\n}\n\nimpl Default for TabConfig {\n    fn default() -> Self {\n        Self {\n            folders_first: true,\n            icon_sizes: IconSizes::default(),\n            military_time: false,\n            show_hidden: false,\n            single_click: false,\n            view: View::List,\n        }\n    }\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]\n#[serde(default)]\npub struct IconSizes {\n    pub list: NonZeroU16,\n    pub grid: NonZeroU16,\n}\n\nimpl Default for IconSizes {\n    fn default() -> Self {\n        Self {\n            list: 100.try_into().unwrap(),\n            grid: 100.try_into().unwrap(),\n        }\n    }\n}\n\nimpl IconSizes {\n    pub fn list(&self) -> u16 {\n        percent!(self.list, ICON_SIZE_LIST) as _\n    }\n\n    pub fn list_condensed(&self) -> u16 {\n        percent!(self.list, ICON_SIZE_LIST_CONDENSED) as _\n    }\n\n    pub fn grid(&self) -> u16 {\n        percent!(self.grid, ICON_SIZE_GRID) as _\n    }\n}\n\npub const TIME_CONFIG_ID: &str = \"com.system76.CosmicAppletTime\";\n\n#[derive(Debug, Default, Clone, CosmicConfigEntry, PartialEq, Eq)]\n#[version = 1]\npub struct TimeConfig {\n    pub military_time: bool,\n}\n"
  },
  {
    "path": "src/context_action.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n\nuse std::path::PathBuf;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::mime_app;\nuse crate::spawn_detached::spawn_detached;\n\n#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]\npub enum ContextActionSelection {\n    #[default]\n    #[serde(alias = \"any\")]\n    Any,\n    #[serde(alias = \"files\")]\n    Files,\n    #[serde(alias = \"folders\")]\n    Folders,\n}\n\n#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]\n#[serde(default)]\npub struct ContextActionPreset {\n    pub name: String,\n    pub confirm: bool,\n    pub selection: ContextActionSelection,\n    pub steps: Vec<String>,\n}\n\nimpl ContextActionPreset {\n    pub fn matches_selection(&self, selected: usize, selected_dir: usize) -> bool {\n        if selected == 0 {\n            return false;\n        }\n\n        match self.selection {\n            ContextActionSelection::Any => true,\n            ContextActionSelection::Files => selected_dir == 0,\n            ContextActionSelection::Folders => selected_dir == selected,\n        }\n    }\n\n    pub fn run(&self, paths: &[PathBuf]) {\n        if self.steps.is_empty() {\n            log::warn!(\"context action {:?} has no steps\", self.name);\n            return;\n        }\n\n        for step in &self.steps {\n            let Some(commands) = mime_app::exec_to_command(step, &self.name, None, paths) else {\n                log::warn!(\n                    \"failed to parse context action {:?}: invalid Exec {:?}\",\n                    self.name,\n                    step\n                );\n                return;\n            };\n\n            for mut command in commands {\n                if let Err(err) = spawn_detached(&mut command) {\n                    log::warn!(\n                        \"failed to run context action {:?} step {:?}: {}\",\n                        self.name,\n                        step,\n                        err\n                    );\n                    return;\n                }\n            }\n        }\n    }\n}\n\npub fn run(actions: &[ContextActionPreset], action: usize, paths: &[PathBuf]) {\n    if let Some(preset) = actions.get(action) {\n        preset.run(paths);\n    } else {\n        log::warn!(\"invalid context action index `{action}`\");\n    }\n}\n"
  },
  {
    "path": "src/dialog.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::app::cosmic::Cosmic;\nuse cosmic::app::{Core, Task, context_drawer};\nuse cosmic::iced::core::SmolStr;\nuse cosmic::iced::core::widget::operation;\nuse cosmic::iced::futures::{self, SinkExt};\nuse cosmic::iced::keyboard::key::Named;\nuse cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};\nuse cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper};\nuse cosmic::iced::widget::scrollable;\nuse cosmic::iced::widget::scrollable::AbsoluteOffset;\nuse cosmic::iced::{\n    self, Alignment, Event, Length, Size, Subscription, event, mouse, stream, window,\n};\nuse cosmic::widget::menu::key_bind::Modifier;\nuse cosmic::widget::menu::{Action as MenuAction, KeyBind};\nuse cosmic::widget::{self, Operation, segmented_button};\nuse cosmic::{Application, ApplicationExt, Element, cosmic_config, cosmic_theme, executor, theme};\nuse mime_guess::{Mime, mime};\nuse notify_debouncer_full::notify::{self, RecommendedWatcher};\nuse notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};\nuse recently_used_xbel::update_recently_used;\nuse rustc_hash::{FxHashMap, FxHashSet};\nuse std::any::TypeId;\nuse std::collections::{HashMap, VecDeque};\nuse std::path::PathBuf;\nuse std::time::{self, Instant};\nuse std::{env, fmt, fs};\n\nuse crate::app::{\n    Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,\n};\nuse crate::config::{\n    Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,\n};\nuse crate::key_bind::key_binds;\nuse crate::localize::LANGUAGE_SORTER;\nuse crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};\nuse crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};\nuse crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};\nuse crate::{fl, home_dir, menu};\n\n#[derive(Clone, Debug)]\npub struct DialogMessage(cosmic::Action<Message>);\n\n#[derive(Clone, Debug)]\npub enum DialogResult {\n    Cancel,\n    Open(Vec<PathBuf>),\n}\n\n#[derive(Clone, Debug)]\npub enum DialogKind {\n    OpenFile,\n    OpenFolder,\n    OpenMultipleFiles,\n    OpenMultipleFolders,\n    SaveFile { filename: String },\n}\n\nimpl DialogKind {\n    pub fn title(&self) -> String {\n        match self {\n            Self::OpenFile => fl!(\"open-file\"),\n            Self::OpenFolder => fl!(\"open-folder\"),\n            Self::OpenMultipleFiles => fl!(\"open-multiple-files\"),\n            Self::OpenMultipleFolders => fl!(\"open-multiple-folders\"),\n            Self::SaveFile { .. } => fl!(\"save-file\"),\n        }\n    }\n\n    pub fn accept_label(&self) -> String {\n        match self {\n            Self::SaveFile { .. } => fl!(\"save\"),\n            _ => fl!(\"open\"),\n        }\n    }\n\n    pub const fn is_dir(&self) -> bool {\n        matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)\n    }\n\n    pub const fn multiple(&self) -> bool {\n        matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders)\n    }\n\n    pub const fn save(&self) -> bool {\n        matches!(self, Self::SaveFile { .. })\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct DialogChoiceOption {\n    pub id: String,\n    pub label: String,\n}\n\nimpl AsRef<str> for DialogChoiceOption {\n    fn as_ref(&self) -> &str {\n        &self.label\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum DialogChoice {\n    CheckBox {\n        id: String,\n        label: String,\n        value: bool,\n    },\n    ComboBox {\n        id: String,\n        label: String,\n        options: Vec<DialogChoiceOption>,\n        selected: Option<usize>,\n    },\n}\n\n#[derive(Clone, Debug)]\npub enum DialogFilterPattern {\n    Glob(String),\n    Mime(String),\n}\n\n#[derive(Clone, Debug)]\npub struct DialogFilter {\n    pub label: String,\n    pub patterns: Vec<DialogFilterPattern>,\n}\n\nimpl AsRef<str> for DialogFilter {\n    fn as_ref(&self) -> &str {\n        &self.label\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct DialogLabelSpan {\n    pub text: String,\n    pub underline: bool,\n}\n\n#[derive(Clone, Debug)]\npub struct DialogLabel {\n    pub spans: Vec<DialogLabelSpan>,\n    pub key_bind_opt: Option<KeyBind>,\n}\n\nimpl<T: AsRef<str>> From<T> for DialogLabel {\n    fn from(text: T) -> Self {\n        let mut spans = Vec::<DialogLabelSpan>::new();\n        let mut key_bind_opt = None;\n        let mut next_underline = false;\n        for c in text.as_ref().chars() {\n            let underline = next_underline;\n            next_underline = false;\n\n            if c == '_' && !underline {\n                next_underline = true;\n                continue;\n            }\n\n            if underline && key_bind_opt.is_none() {\n                key_bind_opt = Some(KeyBind {\n                    modifiers: vec![Modifier::Alt],\n                    key: Key::Character(c.to_lowercase().to_string().into()),\n                });\n            }\n\n            if let Some(span) = spans.last_mut()\n                && underline == span.underline\n            {\n                span.text.push(c);\n                continue;\n            }\n\n            spans.push(DialogLabelSpan {\n                text: String::from(c),\n                underline,\n            });\n        }\n\n        Self {\n            spans,\n            key_bind_opt,\n        }\n    }\n}\n\nimpl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {\n    fn from(label: &'a DialogLabel) -> Self {\n        let mut iced_spans: Vec<cosmic::iced::core::text::Span<'_, ()>> =\n            Vec::with_capacity(label.spans.len());\n        for span in &label.spans {\n            iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));\n        }\n        cosmic::iced::widget::rich_text(iced_spans).into()\n    }\n}\n\npub struct DialogSettings {\n    app_id: String,\n    kind: DialogKind,\n    path_opt: Option<PathBuf>,\n}\n\nimpl DialogSettings {\n    pub fn new() -> Self {\n        Default::default()\n    }\n\n    pub fn app_id(mut self, app_id: String) -> Self {\n        self.app_id = app_id;\n        self\n    }\n\n    pub fn kind(mut self, kind: DialogKind) -> Self {\n        self.kind = kind;\n        self\n    }\n\n    pub fn path(mut self, path: PathBuf) -> Self {\n        self.path_opt = Some(path);\n        self\n    }\n}\n\nimpl Default for DialogSettings {\n    fn default() -> Self {\n        Self {\n            app_id: App::APP_ID.to_string(),\n            kind: DialogKind::OpenFile,\n            path_opt: None,\n        }\n    }\n}\n\npub struct Dialog<M> {\n    cosmic: Cosmic<App>,\n    mapper: fn(DialogMessage) -> M,\n    on_result: Box<dyn Fn(DialogResult) -> M>,\n}\n\nimpl<M: Send + 'static> Dialog<M> {\n    pub fn new(\n        dialog_settings: DialogSettings,\n        mapper: fn(DialogMessage) -> M,\n        on_result: impl Fn(DialogResult) -> M + 'static,\n    ) -> (Self, Task<M>) {\n        //TODO: only do this once somehow?\n        crate::localize::localize();\n\n        let (config_handler, config) = Config::load();\n\n        let mut settings = window::Settings {\n            decorations: false,\n            exit_on_close_request: false,\n            min_size: Some(Size::new(360.0, 180.0)),\n            resizable: true,\n            size: Size::new(1024.0, 640.0),\n            transparent: true,\n            ..Default::default()\n        };\n\n        #[cfg(target_os = \"linux\")]\n        {\n            settings.platform_specific.application_id = dialog_settings.app_id;\n        }\n\n        let (window_id, window_command) = window::open(settings);\n\n        let mut core = Core::default();\n        core.set_main_window_id(Some(window_id));\n        let flags = Flags {\n            kind: dialog_settings.kind,\n            path_opt: dialog_settings.path_opt.as_ref().and_then(|path| {\n                match fs::canonicalize(path) {\n                    Ok(ok) => Some(ok),\n                    Err(err) => {\n                        log::warn!(\"failed to canonicalize {}: {}\", path.display(), err);\n                        None\n                    }\n                }\n            }),\n            window_id,\n            config_handler,\n            config,\n        };\n\n        let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags));\n        (\n            Self {\n                cosmic,\n                mapper,\n                on_result: Box::new(on_result),\n            },\n            Task::batch([\n                window_command.map(|_id| cosmic::action::none()),\n                cosmic_command\n                    .map(DialogMessage)\n                    .map(move |message| cosmic::action::app(mapper(message))),\n            ]),\n        )\n    }\n\n    pub fn set_title(&mut self, title: impl Into<String>) -> Task<M> {\n        let mapper = self.mapper;\n        self.cosmic.app.title = title.into();\n        self.cosmic\n            .app\n            .update_title()\n            .map(DialogMessage)\n            .map(move |message| cosmic::action::app(mapper(message)))\n    }\n\n    pub fn set_accept_label(&mut self, accept_label: impl AsRef<str>) {\n        self.cosmic.app.accept_label = DialogLabel::from(accept_label);\n    }\n\n    pub fn choices(&self) -> &[DialogChoice] {\n        &self.cosmic.app.choices\n    }\n\n    pub fn set_choices(&mut self, choices: impl Into<Vec<DialogChoice>>) {\n        self.cosmic.app.choices = choices.into();\n    }\n\n    pub fn filters(&self) -> (&[DialogFilter], Option<usize>) {\n        (&self.cosmic.app.filters, self.cosmic.app.filter_selected)\n    }\n\n    pub fn set_filters(\n        &mut self,\n        filters: impl Into<Vec<DialogFilter>>,\n        filter_selected: Option<usize>,\n    ) -> Task<M> {\n        let mapper = self.mapper;\n        self.cosmic.app.filters = filters.into();\n        self.cosmic.app.filter_selected = filter_selected;\n        self.cosmic\n            .app\n            .rescan_tab(None)\n            .map(DialogMessage)\n            .map(move |message| cosmic::action::app(mapper(message)))\n    }\n\n    pub fn subscription(&self) -> Subscription<M> {\n        self.cosmic\n            .subscription()\n            .map(DialogMessage)\n            .with(self.mapper)\n            .map(|(mapper, message)| mapper(message))\n    }\n\n    pub fn update(&mut self, message: DialogMessage) -> Task<M> {\n        let mapper = self.mapper;\n        let command = self\n            .cosmic\n            .update(message.0)\n            .map(DialogMessage)\n            .map(move |message| cosmic::action::app(mapper(message)));\n        if let Some(result) = self.cosmic.app.result_opt.take() {\n            #[cfg(feature = \"wayland\")]\n            if !self.cosmic.surface_views.is_empty() {\n                log::debug!(\"waiting for surfaces to close...\");\n                let mut tasks = Vec::new();\n                for id in self.cosmic.surface_views.iter() {\n                    match id.1.1 {\n                        SurfaceIdWrapper::Window(id) => {\n                            tasks.push(window::close::<M>(id).discard());\n                        }\n                        SurfaceIdWrapper::LayerSurface(id) => {\n                            tasks.push(iced_winit::wayland::commands::layer_surface::destroy_layer_surface::<M>(id).discard());\n                        }\n                        SurfaceIdWrapper::Popup(id) => {\n                            tasks.push(\n                                iced_winit::wayland::commands::popup::destroy_popup::<M>(id)\n                                    .discard(),\n                            );\n                        }\n                        SurfaceIdWrapper::Subsurface(id) => {\n                            tasks.push(\n                                iced_winit::wayland::commands::subsurface::destroy_subsurface::<M>(\n                                    id,\n                                )\n                                .discard(),\n                            );\n                        }\n                        _ => {}\n                    }\n                }\n                let on_result_message = (self.on_result)(result);\n\n                tasks.push(Task::future(async move {\n                    cosmic::action::app(on_result_message)\n                }));\n                tasks.push(command);\n                return Task::batch(tasks);\n            }\n            let on_result_message = (self.on_result)(result);\n\n            Task::batch([\n                command,\n                Task::future(async move { cosmic::action::app(on_result_message) }),\n            ])\n        } else {\n            command\n        }\n    }\n\n    pub fn view(&self, window_id: window::Id) -> Element<'_, M> {\n        self.cosmic\n            .view(window_id)\n            .map(DialogMessage)\n            .map(self.mapper)\n    }\n\n    pub const fn window_id(&self) -> window::Id {\n        self.cosmic.app.flags.window_id\n    }\n\n    #[cfg(feature = \"wayland\")]\n    pub fn contains_surface(&self, id: &window::Id) -> bool {\n        self.cosmic.surface_views.contains_key(id)\n    }\n}\n\n#[derive(Clone, Debug)]\nenum DialogPage {\n    NewFolder { parent: PathBuf, name: String },\n    Replace { filename: String },\n}\n\n#[derive(Clone, Debug)]\nstruct Flags {\n    kind: DialogKind,\n    path_opt: Option<PathBuf>,\n    window_id: window::Id,\n    #[allow(dead_code)]\n    config_handler: Option<cosmic_config::Config>,\n    config: Config,\n}\n\n/// Messages that are used specifically by our [`App`].\n#[derive(Clone, Debug)]\nenum Message {\n    None,\n    Cancel,\n    Choice(usize, usize),\n    Config(Config),\n    DialogCancel,\n    DialogComplete,\n    DialogUpdate(DialogPage),\n    Escape,\n    Filename(String),\n    Filter(usize),\n    Key(Modifiers, Key, Option<SmolStr>),\n    ModifiersChanged(Modifiers),\n    MounterItems(MounterKey, MounterItems),\n    Mouse(window::Id, mouse::Button),\n    NewFolder,\n    NotifyEvents(Vec<DebouncedEvent>),\n    NotifyWatcher(WatcherWrapper),\n    Open,\n    Preview,\n    Save(bool),\n    ScrollTab(i16),\n    SearchActivate,\n    SearchClear,\n    SearchInput(String),\n    Surface(cosmic::surface::Action),\n    #[allow(clippy::enum_variant_names)]\n    TabMessage(tab::Message),\n    TabRescan(\n        Location,\n        Option<Box<tab::Item>>,\n        Vec<tab::Item>,\n        Option<Vec<PathBuf>>,\n    ),\n    TabView(tab::View),\n    TimeConfigChange(TimeConfig),\n    ToggleFoldersFirst,\n    ToggleShowHidden,\n    ZoomDefault,\n    ZoomIn,\n    ZoomOut,\n}\n\nimpl From<AppMessage> for Message {\n    fn from(app_message: AppMessage) -> Self {\n        match app_message {\n            AppMessage::None => Self::None,\n            AppMessage::Preview(_entity_opt) => Self::Preview,\n            AppMessage::SearchActivate => Self::SearchActivate,\n            AppMessage::ScrollTab(scroll_speed) => Self::ScrollTab(scroll_speed),\n            AppMessage::TabMessage(_entity_opt, tab_message) => Self::TabMessage(tab_message),\n            AppMessage::TabView(_entity_opt, view) => Self::TabView(view),\n            AppMessage::ToggleFoldersFirst => Self::ToggleFoldersFirst,\n            AppMessage::ToggleShowHidden => Self::ToggleShowHidden,\n            AppMessage::ZoomDefault(_entity_opt) => Self::ZoomDefault,\n            AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn,\n            AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut,\n            AppMessage::NewItem(_entity_opt, true) => Self::NewFolder,\n            AppMessage::Surface(action) => Self::Surface(action),\n            unsupported => {\n                log::warn!(\"{unsupported:?} not supported in dialog mode\");\n                Self::None\n            }\n        }\n    }\n}\n\npub struct MounterData(MounterKey, MounterItem);\n\nstruct WatcherWrapper {\n    watcher_opt: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,\n}\n\nimpl Clone for WatcherWrapper {\n    fn clone(&self) -> Self {\n        Self { watcher_opt: None }\n    }\n}\n\nimpl fmt::Debug for WatcherWrapper {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"WatcherWrapper\").finish()\n    }\n}\n\nimpl PartialEq for WatcherWrapper {\n    fn eq(&self, _other: &Self) -> bool {\n        false\n    }\n}\n\n/// The [`App`] stores application-specific state.\nstruct App {\n    core: Core,\n    flags: Flags,\n    title: String,\n    accept_label: DialogLabel,\n    choices: Vec<DialogChoice>,\n    context_menu_window: Option<window::Id>,\n    context_page: ContextPage,\n    dialog_pages: VecDeque<DialogPage>,\n    dialog_text_input: widget::Id,\n    filters: Vec<DialogFilter>,\n    filter_selected: Option<usize>,\n    filename_id: widget::Id,\n    modifiers: Modifiers,\n    mounter_items: FxHashMap<MounterKey, MounterItems>,\n    nav_model: segmented_button::SingleSelectModel,\n    result_opt: Option<DialogResult>,\n    search_id: widget::Id,\n    tab: Tab,\n    key_binds: HashMap<KeyBind, Action>,\n    watcher_opt: Option<(\n        Debouncer<RecommendedWatcher, RecommendedCache>,\n        FxHashSet<PathBuf>,\n    )>,\n    auto_scroll_speed: Option<i16>,\n    type_select_prefix: String,\n    type_select_last_key: Option<Instant>,\n}\n\nimpl App {\n    fn button_view(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing {\n            space_xxxs,\n            space_xxs,\n            space_xs,\n            space_s,\n            space_l,\n            ..\n        } = theme::spacing();\n        let is_condensed = self.core().is_condensed();\n\n        let mut col = widget::column::with_capacity(2).spacing(space_xxs);\n        if let DialogKind::SaveFile { filename } = &self.flags.kind {\n            col = col.push(\n                widget::text_input(\"\", filename)\n                    .id(self.filename_id.clone())\n                    .double_click_select_delimiter('.')\n                    .on_input(Message::Filename)\n                    .on_submit(|_| Message::Save(false)),\n            );\n        }\n\n        let mut row = widget::row::with_capacity(\n            usize::from(!self.filters.is_empty())\n                + self.choices.len() * 2\n                + if is_condensed { 0 } else { 3 },\n        )\n        .align_y(Alignment::Center)\n        .spacing(space_xxs);\n        if !self.filters.is_empty() {\n            row = row.push(widget::dropdown(\n                &self.filters,\n                self.filter_selected,\n                Message::Filter,\n            ));\n        }\n        for (choice_i, choice) in self.choices.iter().enumerate() {\n            match choice {\n                DialogChoice::CheckBox { label, value, .. } => {\n                    row = row.push(\n                        widget::checkbox(*value)\n                            .label(label)\n                            .on_toggle(move |checked| {\n                                Message::Choice(choice_i, usize::from(checked))\n                            }),\n                    );\n                }\n                DialogChoice::ComboBox {\n                    label,\n                    options,\n                    selected,\n                    ..\n                } => {\n                    row = row.push(widget::text::heading(label));\n                    row = row.push(widget::dropdown(options, *selected, move |option_i| {\n                        Message::Choice(choice_i, option_i)\n                    }));\n                }\n            }\n        }\n\n        if is_condensed {\n            col = col.push(row);\n            row = widget::row::with_capacity(3)\n                .align_y(Alignment::Center)\n                .spacing(space_xxs);\n        }\n        row = row.push(widget::space::horizontal());\n        row = row.push(widget::button::standard(fl!(\"cancel\")).on_press(Message::Cancel));\n\n        let mut has_selected = false;\n        if let Some(items) = self.tab.items_opt() {\n            for item in items {\n                if item.selected {\n                    has_selected = true;\n                    break;\n                }\n            }\n        }\n        row = row.push(\n            //TODO: easier way to create buttons with rich text\n            widget::button::custom(\n                widget::row::with_children([Element::from(&self.accept_label)])\n                    .padding([0, space_s])\n                    .width(Length::Shrink)\n                    .height(space_l)\n                    .spacing(space_xxxs)\n                    .align_y(Alignment::Center)\n            )\n            .padding(0)\n            .on_press_maybe(if self.flags.kind.save() {\n                if let DialogKind::SaveFile { filename } = &self.flags.kind {\n                    (!filename.is_empty()).then_some(Message::Save(false))\n                } else {\n                    None\n                }\n            } else if has_selected || self.flags.kind.is_dir() {\n                Some(Message::Open)\n            } else {\n                None\n            })\n            .class(widget::button::ButtonClass::Suggested)\n            /*TODO: a11y feature: .label(&self.accept_label.text)*/\n        );\n\n        col = col.push(row);\n\n        widget::layer_container(col)\n            .layer(cosmic_theme::Layer::Primary)\n            .padding([8, space_xs])\n            .into()\n    }\n\n    fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> {\n        let military_time = self.tab.config.military_time;\n        let mut children = Vec::with_capacity(1);\n        match kind {\n            PreviewKind::Custom(PreviewItem(item)) => {\n                children.push(item.preview_view(None, military_time));\n            }\n            PreviewKind::Location(location) => {\n                if let Some(items) = self.tab.items_opt() {\n                    for item in items {\n                        if item.location_opt.as_ref() == Some(location) {\n                            children.push(item.preview_view(None, military_time));\n                            // Only show one property view to avoid issues like hangs when generating\n                            // preview images on thousands of files\n                            break;\n                        }\n                    }\n                }\n            }\n            PreviewKind::Selected => {\n                if let Some(items) = self.tab.items_opt() {\n                    let preview_opt = {\n                        let mut selected = items.iter().filter(|item| item.selected);\n\n                        match (selected.next(), selected.next()) {\n                            // At least two selected items\n                            (Some(_), Some(_)) => Some(self.tab.multi_preview_view(None)),\n                            // Exactly one selected item\n                            (Some(item), None) => Some(item.preview_view(None, military_time)),\n                            // No selected items\n                            _ => None,\n                        }\n                    };\n\n                    if let Some(preview) = preview_opt {\n                        children.push(preview);\n                    }\n\n                    if children.is_empty()\n                        && let Some(item) = &self.tab.parent_item_opt\n                    {\n                        children.push(item.preview_view(None, military_time));\n                    }\n                }\n            }\n        }\n        widget::column::with_children(children).into()\n    }\n\n    fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {\n        let location = self.tab.location.clone();\n        let icon_sizes = self.tab.config.icon_sizes;\n        let mounter_items = self.mounter_items.clone();\n        Task::future(async move {\n            let location2 = location.clone();\n            match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {\n                Ok((parent_item_opt, mut items)) => {\n                    #[cfg(feature = \"gvfs\")]\n                    {\n                        let mounter_paths: Box<[_]> = mounter_items\n                            .values()\n                            .flatten()\n                            .filter_map(MounterItem::path)\n                            .collect();\n                        if !mounter_paths.is_empty() {\n                            for item in &mut items {\n                                item.is_mount_point =\n                                    item.path_opt().is_some_and(|p| mounter_paths.contains(p));\n                            }\n                        }\n                    }\n                    cosmic::action::app(Message::TabRescan(\n                        location,\n                        parent_item_opt,\n                        items,\n                        selection_paths,\n                    ))\n                }\n                Err(err) => {\n                    log::warn!(\"failed to rescan: {err}\");\n                    cosmic::action::none()\n                }\n            }\n        })\n    }\n\n    fn search_get(&self) -> Option<&str> {\n        match &self.tab.location {\n            Location::Search(_, term, ..) => Some(term),\n            _ => None,\n        }\n    }\n\n    fn search_set(&mut self, term_opt: Option<String>) -> Task<Message> {\n        let location_opt = match term_opt {\n            Some(term) => {\n                let search_location = if let Some(path) = self.tab.location.path_opt() {\n                    Some(SearchLocation::Path(path.clone()))\n                } else if self.tab.location.is_recents() {\n                    Some(SearchLocation::Recents)\n                } else if self.tab.location.is_trash() {\n                    Some(SearchLocation::Trash)\n                } else {\n                    None\n                };\n\n                search_location.map(|search_location| {\n                    (\n                        Location::Search(\n                            search_location,\n                            term,\n                            self.tab.config.show_hidden,\n                            Instant::now(),\n                        ),\n                        true,\n                    )\n                })\n            }\n            None => match &self.tab.location {\n                Location::Search(search_location, ..) => match search_location {\n                    SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)),\n                    SearchLocation::Recents => Some((Location::Recents, false)),\n                    SearchLocation::Trash => Some((Location::Trash, false)),\n                },\n                _ => None,\n            },\n        };\n        if let Some((location, focus_search)) = location_opt {\n            self.tab.change_location(&location, None);\n            return Task::batch([\n                self.update_title(),\n                self.update_watcher(),\n                self.rescan_tab(None),\n                if focus_search {\n                    widget::text_input::focus(self.search_id.clone())\n                } else {\n                    Task::none()\n                },\n            ]);\n        }\n        Task::none()\n    }\n\n    fn update_config(&mut self) -> Task<Message> {\n        self.core.window.show_context = self.flags.config.dialog.show_details;\n        self.tab.config = self.flags.config.dialog_tab();\n        self.update_nav_model();\n        self.update(Message::TabMessage(tab::Message::Config(self.tab.config)))\n    }\n\n    fn with_dialog_config<F: Fn(&mut DialogConfig)>(&mut self, f: F) -> Task<Message> {\n        let mut dialog = self.flags.config.dialog;\n        f(&mut dialog);\n        if dialog == self.flags.config.dialog {\n            Task::none()\n        } else {\n            if let Some(config_handler) = &self.flags.config_handler {\n                match self.flags.config.set_dialog(config_handler, dialog) {\n                    Ok(_) => {}\n                    Err(err) => {\n                        log::warn!(\"failed to save config \\\"dialog\\\": {err}\");\n                    }\n                }\n            } else {\n                self.flags.config.dialog = dialog;\n                log::warn!(\"failed to save config \\\"dialog\\\": no config handler\",);\n            }\n            self.update_config()\n        }\n    }\n\n    fn activate_nav_model_location(&mut self, location: &Location) {\n        let nav_bar_id = self.nav_model.iter().find(|&id| {\n            self.nav_model\n                .data::<Location>(id)\n                .is_some_and(|l| l == location)\n        });\n\n        if let Some(id) = nav_bar_id {\n            self.nav_model.activate(id);\n        } else {\n            let active = self.nav_model.active();\n            segmented_button::Selectable::deactivate(&mut self.nav_model, active);\n        }\n    }\n\n    fn close_context_menus(&mut self) -> Task<Message> {\n        self.tab.location_context_menu_index = None;\n        if self.tab.context_menu.is_some() {\n            return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));\n        }\n\n        Task::none()\n    }\n\n    fn update_nav_model(&mut self) {\n        let mut nav_model = segmented_button::ModelBuilder::default();\n\n        if self.flags.config.show_recents {\n            nav_model = nav_model.insert(|b| {\n                b.text(fl!(\"recents\"))\n                    .icon(widget::icon::from_name(\"document-open-recent-symbolic\"))\n                    .data(Location::Recents)\n            });\n        }\n\n        for favorite in &self.flags.config.favorites {\n            if let Some(path) = favorite.path_opt() {\n                let name = if matches!(favorite, Favorite::Home) {\n                    fl!(\"home\")\n                } else if let Favorite::Network { name, .. } = favorite {\n                    name.clone()\n                } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {\n                    file_name.to_string()\n                } else {\n                    continue;\n                };\n                nav_model = nav_model.insert(move |b| {\n                    b.text(name.clone())\n                        .icon(\n                            widget::icon::icon(if path.is_dir() {\n                                tab::folder_icon_symbolic(&path, 16)\n                            } else {\n                                widget::icon::from_name(\"text-x-generic-symbolic\")\n                                    .size(16)\n                                    .handle()\n                            })\n                            .size(16),\n                        )\n                        .data(Location::Path(path.clone()))\n                });\n            }\n        }\n\n        // Collect all mounter items\n        let mut nav_items = Vec::new();\n        for (key, items) in &self.mounter_items {\n            nav_items.extend(items.iter().map(|item| (*key, item)));\n        }\n        // Sort by name lexically\n        nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));\n        // Add items to nav model\n        for (i, (key, item)) in nav_items.into_iter().enumerate() {\n            nav_model = nav_model.insert(|mut b| {\n                b = b.text(item.name()).data(MounterData(key, item.clone()));\n                if let Some(path) = item.path() {\n                    b = b.data(Location::Path(path));\n                }\n                if let Some(icon) = item.icon(true) {\n                    b = b.icon(widget::icon::icon(icon).size(16));\n                }\n                if item.is_mounted() {\n                    b = b.closable();\n                }\n                if i == 0 {\n                    b = b.divider_above();\n                }\n                b\n            });\n        }\n\n        self.nav_model = nav_model.build();\n\n        self.activate_nav_model_location(&self.tab.location.clone());\n    }\n\n    fn update_title(&mut self) -> Task<Message> {\n        self.set_header_title(self.title.clone());\n        self.set_window_title(self.title.clone(), self.flags.window_id)\n    }\n\n    fn update_watcher(&mut self) -> Task<Message> {\n        if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {\n            let mut new_paths = FxHashSet::default();\n            if let Some(path) = &self.tab.location.path_opt() {\n                new_paths.insert((*path).clone());\n            }\n\n            // Unwatch paths no longer used\n            for path in &old_paths {\n                if !new_paths.contains(path) {\n                    match watcher.unwatch(path) {\n                        Ok(()) => {\n                            log::debug!(\"unwatching {}\", path.display());\n                        }\n                        Err(err) => {\n                            log::debug!(\"failed to unwatch {}: {}\", path.display(), err);\n                        }\n                    }\n                }\n            }\n\n            // Watch new paths\n            for path in &new_paths {\n                if !old_paths.contains(path) {\n                    //TODO: should this be recursive?\n                    match watcher.watch(path, notify::RecursiveMode::NonRecursive) {\n                        Ok(()) => {\n                            log::debug!(\"watching {}\", path.display());\n                        }\n                        Err(err) => {\n                            log::debug!(\"failed to watch {}: {}\", path.display(), err);\n                        }\n                    }\n                }\n            }\n\n            self.watcher_opt = Some((watcher, new_paths));\n        }\n\n        //TODO: should any of this run in a command?\n        Task::none()\n    }\n}\n\n/// Implement [`Application`] to integrate with COSMIC.\nimpl Application for App {\n    /// Default async executor to use with the app.\n    type Executor = executor::Default;\n\n    /// Argument received\n    type Flags = Flags;\n\n    /// Message type specific to our [`App`].\n    type Message = Message;\n\n    /// The unique application ID to supply to the window manager.\n    const APP_ID: &'static str = \"com.system76.CosmicFilesDialog\";\n\n    fn core(&self) -> &Core {\n        &self.core\n    }\n\n    fn core_mut(&mut self) -> &mut Core {\n        &mut self.core\n    }\n\n    /// Creates the application, and optionally emits command on initialize.\n    fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Message>) {\n        core.window.context_is_overlay = false;\n        core.window.show_close = false;\n        core.window.show_maximize = false;\n        core.window.show_minimize = false;\n\n        let title = flags.kind.title();\n        let accept_label = flags.kind.accept_label();\n\n        let location = Location::Path(match &flags.path_opt {\n            Some(path) => path.clone(),\n            None => match env::current_dir() {\n                Ok(path) => path,\n                Err(_) => home_dir(),\n            },\n        });\n\n        let mut tab = Tab::new(\n            location,\n            flags.config.dialog_tab(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n        tab.mode = tab::Mode::Dialog(flags.kind.clone());\n        tab.sort_name = tab::HeadingOptions::Modified;\n        tab.sort_direction = false;\n\n        let key_binds = key_binds(&tab.mode);\n\n        let mut app = Self {\n            core,\n            flags,\n            title,\n            accept_label: DialogLabel::from(accept_label),\n            choices: Vec::new(),\n            context_menu_window: None,\n            context_page: ContextPage::Preview(None, PreviewKind::Selected),\n            dialog_pages: VecDeque::new(),\n            dialog_text_input: widget::Id::new(\"Dialog Text Input\"),\n            filters: Vec::new(),\n            filter_selected: None,\n            filename_id: widget::Id::new(\"Dialog Filename\"),\n            modifiers: Modifiers::empty(),\n            mounter_items: FxHashMap::default(),\n            nav_model: segmented_button::ModelBuilder::default().build(),\n            result_opt: None,\n            search_id: widget::Id::new(\"Dialog File Search\"),\n            tab,\n            key_binds,\n            watcher_opt: None,\n            auto_scroll_speed: None,\n            type_select_prefix: String::new(),\n            type_select_last_key: None,\n        };\n\n        let commands = Task::batch([\n            app.update_config(),\n            app.update_title(),\n            app.update_watcher(),\n            app.rescan_tab(None),\n        ]);\n\n        (app, commands)\n    }\n\n    fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Message>> {\n        if !self.core.window.show_context {\n            return None;\n        }\n\n        match &self.context_page {\n            ContextPage::Preview(_, kind) => {\n                let actions = self\n                    .tab\n                    .items_opt()\n                    .and_then(|items| {\n                        items\n                            .iter()\n                            .find(|item| item.selected)\n                            .map(|item| item.preview_actions().map(Message::TabMessage))\n                    })\n                    .unwrap_or_else(|| widget::space::horizontal().into());\n                Some(\n                    context_drawer::context_drawer(\n                        self.preview(kind).map(Message::TabMessage),\n                        Message::Preview,\n                    )\n                    .actions(actions),\n                )\n            }\n            _ => None,\n        }\n    }\n\n    fn dialog(&self) -> Option<Element<'_, Message>> {\n        let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();\n\n        //TODO: should gallery view just be a dialog?\n        if self.tab.gallery {\n            return Some(\n                widget::column::with_children([\n                    self.tab.gallery_view().map(Message::TabMessage),\n                    // Draw button row as part of the overlay\n                    widget::container(self.button_view())\n                        .width(Length::Fill)\n                        .padding(space_xxs)\n                        .class(theme::Container::WindowBackground)\n                        .into(),\n                ])\n                .into(),\n            );\n        }\n\n        let dialog_page = self.dialog_pages.front()?;\n\n        let dialog = match dialog_page {\n            DialogPage::NewFolder { parent, name } => {\n                let mut dialog = widget::dialog().title(fl!(\"create-new-folder\"));\n\n                let complete_maybe = if name.is_empty() {\n                    None\n                } else if name == \".\" || name == \"..\" {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\n                        \"name-invalid\",\n                        filename = name.as_str()\n                    )));\n                    None\n                } else if name.contains('/') {\n                    dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-no-slashes\")));\n                    None\n                } else {\n                    let path = parent.join(name);\n                    if path.exists() {\n                        if path.is_dir() {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"folder-already-exists\")));\n                        } else {\n                            dialog = dialog\n                                .tertiary_action(widget::text::body(fl!(\"file-already-exists\")));\n                        }\n                        None\n                    } else {\n                        if name.starts_with('.') {\n                            dialog = dialog.tertiary_action(widget::text::body(fl!(\"name-hidden\")));\n                        }\n                        Some(Message::DialogComplete)\n                    }\n                };\n\n                dialog\n                    .primary_action(\n                        widget::button::suggested(fl!(\"save\"))\n                            .on_press_maybe(complete_maybe.clone()),\n                    )\n                    .secondary_action(\n                        widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                    )\n                    .control(\n                        widget::column::with_children([\n                            widget::text::body(fl!(\"folder-name\")).into(),\n                            widget::text_input(\"\", name.as_str())\n                                .id(self.dialog_text_input.clone())\n                                .on_input(move |name| {\n                                    Message::DialogUpdate(DialogPage::NewFolder {\n                                        parent: parent.clone(),\n                                        name,\n                                    })\n                                })\n                                .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))\n                                .into(),\n                        ])\n                        .spacing(space_xxs),\n                    )\n            }\n            DialogPage::Replace { filename } => widget::dialog()\n                .title(fl!(\"replace-title\", filename = filename.as_str()))\n                .icon(widget::icon::from_name(\"dialog-question\").size(64))\n                .body(fl!(\"replace-warning\"))\n                .primary_action(\n                    widget::button::suggested(fl!(\"replace\"))\n                        .on_press(Message::DialogComplete)\n                        .id(REPLACE_BUTTON_ID.clone()),\n                )\n                .secondary_action(\n                    widget::button::standard(fl!(\"cancel\")).on_press(Message::DialogCancel),\n                ),\n        };\n\n        Some(dialog.into())\n    }\n\n    fn footer(&self) -> Option<Element<'_, Message>> {\n        Some(self.button_view())\n    }\n\n    fn header_end(&self) -> Vec<Element<'_, Message>> {\n        let mut elements = Vec::with_capacity(3);\n\n        if let Some(term) = self.search_get() {\n            if self.core.is_condensed() {\n                elements.push(\n                    //TODO: selected state is not appearing different\n                    widget::button::icon(widget::icon::from_name(\"system-search-symbolic\"))\n                        .on_press(Message::SearchClear)\n                        .padding(8)\n                        .selected(true)\n                        .into(),\n                );\n            } else {\n                elements.push(\n                    widget::text_input::search_input(\"\", term)\n                        .width(Length::Fixed(240.0))\n                        .id(self.search_id.clone())\n                        .on_clear(Message::SearchClear)\n                        .on_input(Message::SearchInput)\n                        .into(),\n                );\n            }\n        } else {\n            elements.push(\n                widget::button::icon(widget::icon::from_name(\"system-search-symbolic\"))\n                    .on_press(Message::SearchActivate)\n                    .padding(8)\n                    .into(),\n            );\n        }\n\n        if self.flags.kind.save() {\n            elements.push(\n                widget::button::icon(widget::icon::from_name(\"folder-new-symbolic\"))\n                    .on_press(Message::NewFolder)\n                    .padding(8)\n                    .into(),\n            );\n        }\n\n        let show_details = match self.context_page {\n            ContextPage::Preview(..) => self.core.window.show_context,\n            _ => false,\n        };\n        elements\n            .push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from));\n\n        elements\n    }\n\n    fn nav_bar(&self) -> Option<Element<'_, cosmic::Action<Self::Message>>> {\n        if !self.core().nav_bar_active() {\n            return None;\n        }\n\n        let nav_model = self.nav_model()?;\n\n        let mut nav = cosmic::widget::nav_bar(nav_model, |entity| {\n            cosmic::action::cosmic(cosmic::app::Action::NavBar(entity))\n        })\n        //TODO .on_close(|entity| cosmic::cosmic::action::app(Message::NavBarClose(entity)))\n        .close_icon(\n            widget::icon::from_name(\"media-eject-symbolic\")\n                .size(16)\n                .icon(),\n        )\n        .into_container();\n\n        if !self.core().is_condensed() {\n            nav = nav.max_width(280);\n        }\n\n        Some(Element::from(\n            nav.width(Length::Shrink).height(Length::Fill),\n        ))\n    }\n\n    fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {\n        Some(&self.nav_model)\n    }\n\n    fn on_app_exit(&mut self) -> Option<Message> {\n        self.result_opt = Some(DialogResult::Cancel);\n        None\n    }\n\n    fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Task<Message> {\n        self.nav_model.activate(entity);\n        if let Some(location) = self.nav_model.data::<Location>(entity) {\n            let message = Message::TabMessage(tab::Message::Location(location.clone()));\n            return self.update(message);\n        }\n\n        if let Some(data) = self.nav_model.data::<MounterData>(entity)\n            && let Some(mounter) = MOUNTERS.get(&data.0)\n        {\n            return mounter\n                .mount(data.1.clone())\n                .map(|()| cosmic::action::none());\n        }\n        Task::none()\n    }\n\n    fn on_escape(&mut self) -> Task<Message> {\n        if self.tab.gallery {\n            // Close gallery if open\n            self.tab.gallery = false;\n            return Task::none();\n        }\n\n        if self.tab.location_context_menu_index.is_some() {\n            self.tab.location_context_menu_index = None;\n            return Task::none();\n        }\n\n        if self.tab.context_menu.is_some() {\n            return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));\n        }\n\n        if self.tab.edit_location.is_some() {\n            // Close location editing if enabled\n            self.tab.edit_location = None;\n            return Task::none();\n        }\n\n        if self.search_get().is_some() {\n            // Close search if open\n            return self.search_set(None);\n        }\n\n        let had_focused_button = self.tab.select_focus_id().is_some();\n        if self.tab.select_none() {\n            if had_focused_button {\n                // Unfocus if there was a focused button\n                return widget::button::focus(widget::Id::unique());\n            }\n            return Task::none();\n        }\n\n        // Close the dialog if the focused widget is the dialog's main text input instead of\n        // unfocussing the widget.\n        if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish()\n            && self.dialog_text_input == focused\n        {\n            return self.update(Message::Cancel);\n        }\n\n        self.update(Message::Cancel)\n    }\n\n    /// Handle application events here.\n    fn update(&mut self, message: Message) -> Task<Message> {\n        match message {\n            Message::None => {}\n            Message::Cancel => {\n                self.result_opt = Some(DialogResult::Cancel);\n                return window::close(self.flags.window_id);\n            }\n            Message::Choice(choice_i, option_i) => {\n                if let Some(choice) = self.choices.get_mut(choice_i) {\n                    match choice {\n                        DialogChoice::CheckBox { value, .. } => *value = option_i > 0,\n                        DialogChoice::ComboBox {\n                            options, selected, ..\n                        } => {\n                            if option_i < options.len() {\n                                *selected = Some(option_i);\n                            } else {\n                                *selected = None;\n                            }\n                        }\n                    }\n                }\n            }\n            Message::Config(config) => {\n                if config != self.flags.config {\n                    log::info!(\"update config\");\n                    self.flags.config = config;\n                    return self.update_config();\n                }\n            }\n            Message::DialogCancel => {\n                self.dialog_pages.pop_front();\n            }\n            Message::DialogComplete => {\n                if let Some(dialog_page) = self.dialog_pages.pop_front() {\n                    match dialog_page {\n                        DialogPage::NewFolder { parent, name } => {\n                            let path = parent.join(name);\n                            match fs::create_dir(&path) {\n                                Ok(()) => {\n                                    // cd to directory\n                                    let message = Message::TabMessage(tab::Message::Location(\n                                        Location::Path(path),\n                                    ));\n                                    return self.update(message);\n                                }\n                                Err(err) => {\n                                    log::warn!(\"failed to create {}: {}\", path.display(), err);\n                                }\n                            }\n                        }\n                        DialogPage::Replace { .. } => {\n                            return self.update(Message::Save(true));\n                        }\n                    }\n                }\n            }\n            Message::DialogUpdate(dialog_page) => {\n                if !self.dialog_pages.is_empty() {\n                    self.dialog_pages[0] = dialog_page;\n                }\n            }\n            Message::Escape => return self.on_escape(),\n            Message::Filename(new_filename) => {\n                // Select based on filename\n                self.tab.select_name(&new_filename);\n\n                if let DialogKind::SaveFile { filename } = &mut self.flags.kind {\n                    *filename = new_filename;\n                }\n            }\n            Message::Filter(filter_i) => {\n                if filter_i < self.filters.len() {\n                    self.filter_selected = Some(filter_i);\n                } else {\n                    self.filter_selected = None;\n                }\n                return self.rescan_tab(None);\n            }\n            Message::Key(modifiers, key, text) => {\n                for (key_bind, action) in &self.key_binds {\n                    if key_bind.matches(modifiers, &key) {\n                        return self.update(Message::from(action.message()));\n                    }\n                }\n\n                // Check key binds from accept label\n                if let Some(key_bind) = &self.accept_label.key_bind_opt\n                    && key_bind.matches(modifiers, &key)\n                {\n                    return self.update(if self.flags.kind.save() {\n                        Message::Save(false)\n                    } else {\n                        Message::Open\n                    });\n                }\n\n                // Uncaptured keys with only shift modifiers go to the search or location box\n                if !modifiers.logo()\n                    && !modifiers.control()\n                    && !modifiers.alt()\n                    && matches!(key, Key::Character(_))\n                    && let Some(text) = text\n                {\n                    match self.flags.config.type_to_search {\n                        TypeToSearch::Recursive => {\n                            let mut term = self.search_get().unwrap_or_default().to_string();\n                            term.push_str(&text);\n                            return self.search_set(Some(term));\n                        }\n                        TypeToSearch::EnterPath => {\n                            let location = (self.tab.edit_location)\n                                .as_ref()\n                                .map_or_else(|| &self.tab.location, |x| &x.location);\n                            // Try to add text to end of location\n                            if let Some(path) = location.path_opt() {\n                                let mut path_string = path.to_string_lossy().to_string();\n                                path_string.push_str(&text);\n                                self.tab.edit_location =\n                                    Some(location.with_path(PathBuf::from(path_string)).into());\n                            }\n                        }\n                        TypeToSearch::SelectByPrefix => {\n                            // Reset buffer if timeout elapsed\n                            if let Some(last_key) = self.type_select_last_key\n                                && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT\n                            {\n                                self.type_select_prefix.clear();\n                            }\n\n                            // Accumulate character and select\n                            self.type_select_prefix.push_str(&text.to_lowercase());\n                            self.type_select_last_key = Some(Instant::now());\n\n                            self.tab.select_by_prefix(&self.type_select_prefix);\n                            if let Some(offset) = self.tab.select_focus_scroll() {\n                                return scrollable::scroll_to(\n                                    self.tab.scrollable_id.clone(),\n                                    AbsoluteOffset {\n                                        x: Some(offset.x),\n                                        y: Some(offset.y),\n                                    },\n                                );\n                            }\n                        }\n                    }\n                }\n            }\n            Message::ModifiersChanged(modifiers) => {\n                self.modifiers = modifiers;\n            }\n            Message::MounterItems(mounter_key, mounter_items) => {\n                // Check for unmounted folders\n                let mut unmounted = Vec::new();\n                if let Some(old_items) = self.mounter_items.get(&mounter_key) {\n                    for old_item in old_items {\n                        if let Some(old_path) = old_item.path()\n                            && old_item.is_mounted()\n                        {\n                            let mut still_mounted = false;\n                            for item in &mounter_items {\n                                if let Some(path) = item.path()\n                                    && path == old_path\n                                    && item.is_mounted()\n                                {\n                                    still_mounted = true;\n                                    break;\n                                }\n                            }\n                            if !still_mounted {\n                                unmounted.push(Location::Path(old_path));\n                            }\n                        }\n                    }\n                }\n\n                // Go back to home in any tabs that were unmounted\n                let mut commands = Vec::new();\n                {\n                    let home_location = Location::Path(home_dir());\n                    if unmounted.contains(&self.tab.location) {\n                        self.tab.change_location(&home_location, None);\n                        commands.push(self.update_watcher());\n                        commands.push(self.rescan_tab(None));\n                    }\n                }\n\n                // Insert new items\n                self.mounter_items.insert(mounter_key, mounter_items);\n\n                // Update nav bar\n                //TODO: this could change favorites IDs while they are in use\n                self.update_nav_model();\n\n                return Task::batch(commands);\n            }\n            Message::Mouse(window_id, _button) => {\n                // Close context menu when clicking outside.\n                if self.core.main_window_id() == Some(window_id) {\n                    return self.close_context_menus();\n                }\n            }\n            Message::NewFolder => {\n                if let Some(path) = self.tab.location.path_opt() {\n                    self.dialog_pages.push_back(DialogPage::NewFolder {\n                        parent: path.clone(),\n                        name: String::new(),\n                    });\n                    return widget::text_input::focus(self.dialog_text_input.clone());\n                }\n            }\n            Message::NotifyEvents(events) => {\n                log::debug!(\"{events:?}\");\n\n                if let Some(path) = self.tab.location.path_opt() {\n                    let mut contains_change = false;\n                    for event in &events {\n                        for event_path in &event.paths {\n                            if event_path.starts_with(path) {\n                                if let notify::EventKind::Modify(\n                                    notify::event::ModifyKind::Metadata(_)\n                                    | notify::event::ModifyKind::Data(_),\n                                ) = event.kind\n                                {\n                                    // If metadata or data changed, find the matching item and reload it\n                                    //TODO: this could be further optimized by looking at what exactly changed\n                                    if let Some(items) = &mut self.tab.items_opt {\n                                        for item in items.iter_mut() {\n                                            if item.path_opt() == Some(event_path) {\n                                                //TODO: reload more, like mime types?\n                                                match fs::metadata(event_path) {\n                                                    Ok(new_metadata) => {\n                                                        if let ItemMetadata::Path {\n                                                            metadata, ..\n                                                        } = &mut item.metadata\n                                                        {\n                                                            *metadata = new_metadata;\n                                                        }\n                                                    }\n                                                    Err(err) => {\n                                                        log::warn!(\n                                                            \"failed to reload metadata for {}: {}\",\n                                                            path.display(),\n                                                            err\n                                                        );\n                                                    }\n                                                }\n                                                //TODO item.thumbnail_opt =\n                                            }\n                                        }\n                                    }\n                                } else {\n                                    // Any other events reload the whole tab\n                                    contains_change = true;\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                    if contains_change {\n                        return self.rescan_tab(None);\n                    }\n                }\n            }\n            Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()\n            {\n                Some(watcher) => {\n                    self.watcher_opt = Some((watcher, FxHashSet::default()));\n                    return self.update_watcher();\n                }\n                None => {\n                    log::warn!(\"message did not contain notify watcher\");\n                }\n            },\n            Message::Open => {\n                let mut paths = Vec::new();\n                if let Some(items) = self.tab.items_opt() {\n                    for item in items {\n                        if item.selected\n                            && let Some(path) = item.path_opt()\n                        {\n                            paths.push(path.clone());\n                            if self.flags.config.show_recents {\n                                let _ = update_recently_used(\n                                    path,\n                                    Self::APP_ID.to_string(),\n                                    \"cosmic-files\".to_string(),\n                                    None,\n                                );\n                            }\n                        }\n                    }\n                }\n\n                // Ensure selection is allowed\n                //TODO: improve tab logic so this doesn't block the open button so often\n                for path in &paths {\n                    let path_is_dir = path.is_dir();\n                    if path_is_dir != self.flags.kind.is_dir() {\n                        if path_is_dir && paths.len() == 1 {\n                            // If the only selected item is a directory and we are selecting files, cd to it\n                            let message = Message::TabMessage(tab::Message::Location(\n                                Location::Path(path.clone()),\n                            ));\n                            return self.update(message);\n                        }\n\n                        // Otherwise, this is not a legal selection\n                        return Task::none();\n                    }\n                }\n\n                // If there are proper matching items, return them\n                if !paths.is_empty() {\n                    self.result_opt = Some(DialogResult::Open(paths));\n                    return window::close(self.flags.window_id);\n                }\n\n                // If we are in directory mode, return the current directory\n                if self.flags.kind.is_dir()\n                    && let Location::Path(tab_path) = &self.tab.location\n                {\n                    self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));\n                    return window::close(self.flags.window_id);\n                }\n            }\n            Message::Preview => {\n                self.context_page = ContextPage::Preview(None, PreviewKind::Selected);\n                return self.with_dialog_config(|config| {\n                    config.show_details = !config.show_details;\n                });\n            }\n            Message::Save(replace) => {\n                if let DialogKind::SaveFile { filename } = &self.flags.kind\n                    && !filename.is_empty()\n                    && let Some(tab_path) = self.tab.location.path_opt()\n                {\n                    let path = tab_path.join(filename);\n                    if path.is_dir() {\n                        // cd to directory\n                        let message =\n                            Message::TabMessage(tab::Message::Location(Location::Path(path)));\n                        return self.update(message);\n                    } else if !replace && path.exists() {\n                        self.dialog_pages.push_back(DialogPage::Replace {\n                            filename: filename.clone(),\n                        });\n                        return widget::button::focus(REPLACE_BUTTON_ID.clone());\n                    }\n                    self.result_opt = Some(DialogResult::Open(vec![path]));\n                    return window::close(self.flags.window_id);\n                }\n            }\n            Message::ScrollTab(scroll_speed) => {\n                return self.update(Message::TabMessage(tab::Message::ScrollTab(\n                    f32::from(scroll_speed) / 10.0,\n                )));\n            }\n            Message::SearchActivate => {\n                let mut tasks = vec![self.close_context_menus()];\n\n                if self.search_get().is_none() {\n                    tasks.push(self.search_set(Some(String::new())));\n                } else {\n                    tasks.push(widget::text_input::focus(self.search_id.clone()));\n                }\n\n                return Task::batch(tasks);\n            }\n            Message::SearchClear => {\n                return Task::batch([self.close_context_menus(), self.search_set(None)]);\n            }\n            Message::SearchInput(input) => {\n                return self.search_set(Some(input));\n            }\n            Message::TabMessage(tab_message) => {\n                let click_i_opt = match tab_message {\n                    tab::Message::Click(click_i_opt) => click_i_opt,\n                    _ => None,\n                };\n\n                let tab_commands = self.tab.update(tab_message, self.modifiers);\n\n                // Update filename box when anything is selected\n                if let DialogKind::SaveFile { filename } = &mut self.flags.kind\n                    && let Some(click_i) = click_i_opt\n                    && let Some(items) = self.tab.items_opt()\n                    && let Some(item) = items.get(click_i)\n                    && item.selected\n                    && !item.metadata.is_dir()\n                {\n                    filename.clone_from(&item.name);\n                }\n\n                let mut commands = Vec::new();\n                for tab_command in tab_commands {\n                    match tab_command {\n                        tab::Command::Action(action) => {\n                            commands.push(self.update(Message::from(action.message())));\n                        }\n                        tab::Command::ChangeLocation(_tab_title, _tab_path, selection_paths) => {\n                            commands.push(Task::batch([\n                                self.update_watcher(),\n                                self.rescan_tab(selection_paths),\n                            ]));\n                        }\n                        tab::Command::ContextMenu(point_opt, parent_id) => {\n                            #[cfg(feature = \"wayland\")]\n                            match point_opt {\n                                Some(point) => {\n                                    if crate::is_wayland() {\n                                        // Open context menu\n                                        use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{\n                                            Anchor, Gravity,\n                                        };\n                                        use cosmic::iced::runtime::platform_specific::wayland::popup::{\n                                            SctkPopupSettings, SctkPositioner,\n                                        };\n                                        use cosmic::iced::Rectangle;\n                                        let window_id = window::Id::unique();\n                                        self.context_menu_window = Some(window_id);\n                                        let autosize_id = widget::Id::unique();\n                                        commands.push(self.update(Message::Surface(\n                                            cosmic::surface::action::app_popup(\n                                                move |app: &mut Self| -> SctkPopupSettings {\n                                                    let anchor_rect = Rectangle {\n                                                        x: point.x as i32,\n                                                        y: point.y as i32,\n                                                        width: 1,\n                                                        height: 1,\n                                                    };\n                                                    let positioner = SctkPositioner {\n                                                        size: None,\n                                                        anchor_rect,\n                                                        anchor: Anchor::None,\n                                                        gravity: Gravity::BottomRight,\n                                                        reactive: true,\n                                                        ..Default::default()\n                                                    };\n                                                    SctkPopupSettings {\n                                                        parent: parent_id\n                                                            .unwrap_or(app.flags.window_id),\n                                                        id: window_id,\n                                                        positioner,\n                                                        parent_size: None,\n                                                        grab: true,\n                                                        close_with_children: false,\n                                                        input_zone: None,\n                                                    }\n                                                },\n                                                Some(Box::new(move |app: &Self| {\n                                                    widget::autosize::autosize(\n                                                        menu::context_menu(\n                                                            &app.tab,\n                                                            &app.key_binds,\n                                                            &app.modifiers,\n                                                            false, // Paste not used in dialogs\n                                                            &app.flags.config.context_actions,\n                                                        )\n                                                        .map(Message::TabMessage)\n                                                        .map(cosmic::Action::App),\n                                                        autosize_id.clone(),\n                                                    )\n                                                    .into()\n                                                })),\n                                            ),\n                                        )));\n                                    }\n                                }\n                                None => {\n                                    if let Some(window_id) = self.context_menu_window.take() {\n                                        commands.push(self.update(Message::Surface(\n                                            cosmic::surface::action::destroy_popup(window_id),\n                                        )));\n                                    }\n                                }\n                            }\n                        }\n                        tab::Command::Iced(iced_command) => {\n                            commands.push(iced_command.0.map(|tab_message| {\n                                cosmic::action::app(Message::TabMessage(tab_message))\n                            }));\n                        }\n                        tab::Command::OpenFile(_item_path) => {\n                            if self.flags.kind.save() {\n                                commands.push(self.update(Message::Save(false)));\n                            } else {\n                                commands.push(self.update(Message::Open));\n                            }\n                        }\n                        tab::Command::Preview(kind) => {\n                            self.context_page = ContextPage::Preview(None, kind);\n                            commands.push(self.with_dialog_config(|config| {\n                                config.show_details = true;\n                            }));\n                        }\n                        tab::Command::WindowDrag => {\n                            commands.push(window::drag(self.flags.window_id));\n                        }\n                        tab::Command::WindowToggleMaximize => {\n                            commands.push(window::toggle_maximize(self.flags.window_id));\n                        }\n                        tab::Command::AutoScroll(scroll_speed) => {\n                            // converting an f32 to an i16 here by multiplying by 10 and casting to i16\n                            // further resolution isn't necessary\n                            if let Some(scroll_speed_float) = scroll_speed {\n                                self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16);\n                            } else {\n                                self.auto_scroll_speed = None;\n                            }\n                        }\n                        unsupported => {\n                            log::warn!(\"{unsupported:?} not supported in dialog mode\");\n                        }\n                    }\n                }\n                return Task::batch(commands);\n            }\n            Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => {\n                if location == self.tab.location {\n                    // Filter\n                    if let Some(filter_i) = self.filter_selected\n                        && let Some(filter) = self.filters.get(filter_i)\n                    {\n                        let mut parsed_globs = Vec::new();\n                        let mut mimes = Vec::new();\n                        for pattern in &filter.patterns {\n                            match pattern {\n                                DialogFilterPattern::Glob(value) => {\n                                    match glob::Pattern::new(value) {\n                                        Ok(glob) => parsed_globs.push(glob),\n                                        Err(err) => {\n                                            log::warn!(\"failed to parse glob {value:?}: {err}\");\n                                        }\n                                    }\n                                }\n                                DialogFilterPattern::Mime(value) => match value.parse::<Mime>() {\n                                    Ok(parsed) => mimes.push(parsed),\n                                    Err(err) => {\n                                        log::warn!(\"failed to parse mime {value:?}: {err}\");\n                                    }\n                                },\n                            }\n                        }\n\n                        items.retain(|item| {\n                            // Directories are always shown\n                            item.metadata.is_dir()\n                                    || mimes.iter().any(|filter_mime| {\n                                        if filter_mime.subtype() == mime::STAR {\n                                            filter_mime.type_() == item.mime.type_()\n                                        } else {\n                                            *filter_mime == item.mime\n                                        }\n                                    })\n                                // Check for glob match (last because it is slower)\n                                    || parsed_globs.iter().any(|glob| glob.matches(&item.name))\n                        });\n                    }\n\n                    // Select based on filename\n                    if let DialogKind::SaveFile { filename } = &self.flags.kind {\n                        for item in &mut items {\n                            item.selected = &item.name == filename;\n                        }\n                    }\n\n                    self.tab.parent_item_opt = parent_item_opt;\n                    self.tab.set_items(items);\n\n                    if let Some(mut selection_paths) = selection_paths {\n                        if !self.flags.kind.multiple() {\n                            selection_paths.truncate(1);\n                        }\n                        self.tab.select_paths(selection_paths);\n                    }\n\n                    // Reset focus on location change\n                    if self.search_get().is_some() {\n                        return widget::text_input::focus(self.search_id.clone());\n                    }\n                    if let DialogKind::SaveFile { filename } = &self.flags.kind {\n                        return Task::batch([\n                            widget::text_input::focus(self.filename_id.clone()),\n                            widget::text_input::select_until_last(\n                                self.filename_id.clone(),\n                                filename,\n                                '.',\n                            ),\n                        ]);\n                    }\n                    return widget::text_input::focus(self.filename_id.clone());\n                }\n            }\n            Message::TabView(view) => {\n                return self.with_dialog_config(|config| {\n                    config.view = view;\n                });\n            }\n            Message::TimeConfigChange(time_config) => {\n                self.flags.config.tab.military_time = time_config.military_time;\n                return self.update_config();\n            }\n            Message::ToggleFoldersFirst => {\n                return self.with_dialog_config(|config| {\n                    config.folders_first = !config.folders_first;\n                });\n            }\n            Message::ToggleShowHidden => {\n                return self.with_dialog_config(|config| {\n                    config.show_hidden = !config.show_hidden;\n                });\n            }\n            Message::ZoomDefault => {\n                return self.with_dialog_config(|config| {\n                    zoom_to_default(config.view, &mut config.icon_sizes);\n                });\n            }\n            Message::ZoomIn => {\n                return self.with_dialog_config(|config| {\n                    zoom_in_view(config.view, &mut config.icon_sizes);\n                });\n            }\n            Message::ZoomOut => {\n                return self.with_dialog_config(|config| {\n                    zoom_out_view(config.view, &mut config.icon_sizes);\n                });\n            }\n            Message::Surface(action) => {\n                return cosmic::task::message(cosmic::Action::Cosmic(\n                    cosmic::app::Action::Surface(action),\n                ));\n            }\n        }\n\n        Task::none()\n    }\n\n    /// Creates a view after each update.\n    fn view(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();\n\n        let mut col = widget::column::with_capacity(2);\n\n        if self.core.is_condensed()\n            && let Some(term) = self.search_get()\n        {\n            col = col.push(\n                widget::container(\n                    widget::text_input::search_input(\"\", term)\n                        .width(Length::Fill)\n                        .id(self.search_id.clone())\n                        .on_clear(Message::SearchClear)\n                        .on_input(Message::SearchInput),\n                )\n                .padding(space_xxs),\n            );\n        }\n\n        col = col.push(\n            self.tab\n                .view(&self.key_binds, &self.modifiers, false, &[])\n                .map(Message::TabMessage),\n        );\n\n        col.into()\n    }\n\n    fn subscription(&self) -> Subscription<Message> {\n        struct WatcherSubscription;\n        struct TimeSubscription;\n        let mut subscriptions = vec![\n            event::listen_with(|event, status, window_id| match event {\n                Event::Mouse(mouse::Event::ButtonPressed(button)) => match status {\n                    event::Status::Ignored => Some(Message::Mouse(window_id, button)),\n                    event::Status::Captured => None,\n                },\n                Event::Keyboard(KeyEvent::KeyPressed {\n                    key,\n                    modifiers,\n                    text,\n                    ..\n                }) => match status {\n                    event::Status::Ignored => Some(Message::Key(modifiers, key, text)),\n                    event::Status::Captured => {\n                        if key == Key::Named(Named::Escape) {\n                            Some(Message::Escape)\n                        } else {\n                            None\n                        }\n                    }\n                },\n                Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {\n                    Some(Message::ModifiersChanged(modifiers))\n                }\n                _ => None,\n            }),\n            Config::subscription().map(|update| {\n                if !update.errors.is_empty() {\n                    log::info!(\n                        \"errors loading config {:?}: {:?}\",\n                        update.keys,\n                        update.errors\n                    );\n                }\n                Message::Config(update.config)\n            }),\n            cosmic_config::config_subscription::<_, TimeConfig>(\n                TypeId::of::<TimeSubscription>(),\n                TIME_CONFIG_ID.into(),\n                1,\n            )\n            .map(|update| {\n                if !update.errors.is_empty() {\n                    log::info!(\n                        \"errors loading time config {:?}: {:?}\",\n                        update.keys,\n                        update.errors\n                    );\n                }\n                Message::TimeConfigChange(update.config)\n            }),\n            Subscription::run_with(TypeId::of::<WatcherSubscription>(), |_| {\n                stream::channel(100, {\n                    |mut output: futures::channel::mpsc::Sender<_>| async move {\n                        let watcher_res = {\n                            let mut output = output.clone();\n                            new_debouncer(\n                                time::Duration::from_millis(250),\n                                Some(time::Duration::from_millis(250)),\n                                move |events_res: notify_debouncer_full::DebounceEventResult| {\n                                    match events_res {\n                                        Ok(mut events) => {\n                                            events.retain(|event| {\n                                            match &event.kind {\n                                                notify::EventKind::Access(_) => {\n                                                    // Data not mutated\n                                                    false\n                                                }\n                                                notify::EventKind::Modify(\n                                                    notify::event::ModifyKind::Metadata(e),\n                                                ) if (*e != notify::event::MetadataKind::Any\n                                                    && *e\n                                                        != notify::event::MetadataKind::WriteTime) =>\n                                                {\n                                                    // Data not mutated nor modify time changed\n                                                    false\n                                                }\n                                                _ => true\n                                            }\n                                        });\n\n                                            if !events.is_empty() {\n                                                match futures::executor::block_on(async {\n                                                    output.send(Message::NotifyEvents(events)).await\n                                                }) {\n                                                    Ok(()) => {}\n                                                    Err(err) => {\n                                                        log::warn!(\n                                                            \"failed to send notify events: {err:?}\"\n                                                        );\n                                                    }\n                                                }\n                                            }\n                                        }\n                                        Err(err) => {\n                                            log::warn!(\"failed to watch files: {err:?}\");\n                                        }\n                                    }\n                                },\n                            )\n                        };\n\n                        match watcher_res {\n                            Ok(watcher) => {\n                                match output\n                                    .send(Message::NotifyWatcher(WatcherWrapper {\n                                        watcher_opt: Some(watcher),\n                                    }))\n                                    .await\n                                {\n                                    Ok(()) => {}\n                                    Err(err) => {\n                                        log::warn!(\"failed to send notify watcher: {err:?}\");\n                                    }\n                                }\n                            }\n                            Err(err) => {\n                                log::warn!(\"failed to create file watcher: {err:?}\");\n                            }\n                        }\n\n                        std::future::pending().await\n                    }\n                })\n            }),\n            self.tab\n                .subscription(\n                    self.core.window.show_context\n                        && matches!(\n                            self.context_page,\n                            ContextPage::Preview(_, PreviewKind::Selected)\n                        ),\n                )\n                .map(Message::TabMessage),\n        ];\n\n        if let Some(scroll_speed) = self.auto_scroll_speed {\n            subscriptions.push(\n                iced::time::every(time::Duration::from_millis(10))\n                    .with(scroll_speed)\n                    .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)),\n            );\n        }\n\n        subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {\n            mounter\n                .subscription()\n                .with(*key)\n                .map(|(key, mounter_message)| {\n                    if let MounterMessage::Items(items) = mounter_message {\n                        Message::MounterItems(key, items)\n                    } else {\n                        log::warn!(\"{mounter_message:?} not supported in dialog mode\");\n                        Message::None\n                    }\n                })\n        }));\n\n        Subscription::batch(subscriptions)\n    }\n}\n"
  },
  {
    "path": "src/key_bind.rs",
    "content": "use cosmic::iced::core::keyboard::key::Named;\nuse cosmic::iced::keyboard::Key;\nuse cosmic::widget::menu::key_bind::{KeyBind, Modifier};\nuse std::collections::HashMap;\n\nuse crate::app::Action;\nuse crate::tab;\n\n//TODO: load from config\npub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {\n    let mut key_binds = HashMap::new();\n\n    macro_rules! bind {\n        ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{\n            key_binds.insert(\n                KeyBind {\n                    modifiers: vec![$(Modifier::$modifier),*],\n                    key: $key,\n                },\n                Action::$action,\n            );\n        }};\n    }\n\n    // Common keys\n    bind!([], Key::Named(Named::ArrowDown), ItemDown);\n    bind!([], Key::Named(Named::ArrowLeft), ItemLeft);\n    bind!([], Key::Named(Named::ArrowRight), ItemRight);\n    bind!([], Key::Named(Named::ArrowUp), ItemUp);\n    bind!([], Key::Named(Named::F5), Reload);\n    bind!([], Key::Named(Named::Home), SelectFirst);\n    bind!([], Key::Named(Named::End), SelectLast);\n    bind!([Shift], Key::Named(Named::ArrowDown), ItemDown);\n    bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);\n    bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);\n    bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);\n    bind!([Shift], Key::Named(Named::Home), SelectFirst);\n    bind!([Shift], Key::Named(Named::End), SelectLast);\n    bind!([Ctrl, Shift], Key::Character(\"n\".into()), NewFolder);\n    bind!([], Key::Named(Named::Enter), Open);\n    bind!([Ctrl], Key::Character(\" \".into()), Preview);\n    bind!([], Key::Character(\" \".into()), Gallery);\n\n    bind!([Ctrl], Key::Character(\"h\".into()), ToggleShowHidden);\n    bind!([Ctrl], Key::Character(\"a\".into()), SelectAll);\n    bind!([Ctrl], Key::Character(\"=\".into()), ZoomIn);\n    bind!([Ctrl], Key::Character(\"+\".into()), ZoomIn);\n    bind!([Ctrl], Key::Character(\"0\".into()), ZoomDefault);\n    bind!([Ctrl], Key::Character(\"-\".into()), ZoomOut);\n    // Switch view\n    bind!([Ctrl], Key::Character(\"1\".into()), TabViewList);\n    bind!([Ctrl], Key::Character(\"2\".into()), TabViewGrid);\n\n    // App-only keys\n    if matches!(mode, tab::Mode::App) {\n        bind!([Ctrl], Key::Character(\"d\".into()), AddToSidebar);\n        bind!([Ctrl], Key::Named(Named::Enter), OpenInNewTab);\n        bind!([Ctrl], Key::Character(\",\".into()), Settings);\n        bind!([Ctrl], Key::Character(\"w\".into()), TabClose);\n        bind!([Ctrl], Key::Character(\"t\".into()), TabNew);\n        bind!([Ctrl], Key::Named(Named::Tab), TabNext);\n        bind!([Ctrl, Shift], Key::Named(Named::Tab), TabPrev);\n        bind!([Ctrl], Key::Character(\"q\".into()), WindowClose);\n        bind!([Ctrl], Key::Character(\"n\".into()), WindowNew);\n    }\n\n    // App and desktop only keys\n    if matches!(mode, tab::Mode::App | tab::Mode::Desktop) {\n        bind!([Ctrl], Key::Character(\"c\".into()), Copy);\n        bind!([Ctrl, Shift], Key::Character(\"c\".into()), CopyPath);\n        bind!([Ctrl], Key::Character(\"x\".into()), Cut);\n        bind!([], Key::Named(Named::Delete), Delete);\n        bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete);\n        bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow);\n        bind!([Ctrl], Key::Character(\"v\".into()), Paste);\n        bind!([], Key::Named(Named::F2), Rename);\n    }\n\n    // App and dialog only keys\n    if matches!(mode, tab::Mode::App | tab::Mode::Dialog(_)) {\n        bind!([Ctrl], Key::Character(\"l\".into()), EditLocation);\n        bind!([Alt], Key::Named(Named::ArrowRight), HistoryNext);\n        bind!([Alt], Key::Named(Named::ArrowLeft), HistoryPrevious);\n        bind!([], Key::Named(Named::Backspace), HistoryPrevious);\n        bind!([Alt], Key::Named(Named::ArrowUp), LocationUp);\n        bind!([Ctrl], Key::Character(\"f\".into()), SearchActivate);\n    }\n\n    key_binds\n}\n"
  },
  {
    "path": "src/large_image.rs",
    "content": "use cosmic::widget;\nuse image::ImageReader;\nuse std::collections::{HashMap, HashSet};\nuse std::path::{Path, PathBuf};\n\n/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes)\npub const RGBA_BYTES_PER_PIXEL: u64 = 4;\n\n/// System memory reserve in MB to maintain for system stability (prevents thrashing)\n/// Note: RAM checking is currently only available on Linux via procfs.\n/// On Windows and macOS, only GPU buffer limits are enforced.\nconst SYSTEM_MEMORY_RESERVE_MB: u64 = 500;\n\n/// Maximum memory allocation for gallery image decoding in MB.\n/// Gallery mode uses the full memory budget since only one image decodes at a time.\n/// This matches the ThumbCfg max_mem_mb budget for consistency.\nconst GALLERY_MEMORY_LIMIT_MB: u64 = 2000;\n\n/// Threshold for considering an image \"large\" requiring GPU tiling\n/// Atlas fragment/tile size in pixels. Large images are split into fragments of this size.\n/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs\npub const ATLAS_FRAGMENT_SIZE: u32 = 4096;\n\n/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations)\npub const MB_TO_BYTES: u64 = 1024 * 1024;\n\n/// Conversion factor: 1 MB = 1000 * 1000 bytes (decimal megabyte, used by image crate)\n/// The image crate's memory limits use decimal MB, not binary MB.\npub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000;\n\n/// Scale factor for HiDPI displays - decode at higher resolution than display size\n/// for better quality on high-DPI screens. 1.5x provides good balance between\n/// quality and memory usage and also prevets re-decoding on small windows size adjustments.\nconst DISPLAY_SCALE_FACTOR: f32 = 1.5;\n\n/// Calculate optimal target dimensions for decoding based on display size.\n/// Returns None if no resizing is needed (image is smaller than display).\n///\n/// This helps reduce memory usage by decoding large images at a resolution\n/// appropriate for the display, rather than always using full resolution.\npub fn calculate_target_dimensions(\n    image_width: u32,\n    image_height: u32,\n    display_width: u32,\n    display_height: u32,\n) -> Option<(u32, u32)> {\n    let target_width = (display_width as f32 * DISPLAY_SCALE_FACTOR) as u32;\n    let target_height = (display_height as f32 * DISPLAY_SCALE_FACTOR) as u32;\n\n    if image_width <= target_width && image_height <= target_height {\n        return None;\n    }\n\n    let image_aspect = image_width as f32 / image_height as f32;\n    let target_aspect = target_width as f32 / target_height as f32;\n\n    let (new_width, new_height) = if image_aspect > target_aspect {\n        let w = target_width;\n        let h = (target_width as f32 / image_aspect) as u32;\n        (w, h)\n    } else {\n        let h = target_height;\n        let w = (target_height as f32 * image_aspect) as u32;\n        (w, h)\n    };\n\n    let new_width = new_width.max(1);\n    let new_height = new_height.max(1);\n\n    log::info!(\n        \"Calculated target dimensions: {}x{} -> {}x{} (display: {}x{}, scale: {}x)\",\n        image_width,\n        image_height,\n        new_width,\n        new_height,\n        display_width,\n        display_height,\n        DISPLAY_SCALE_FACTOR\n    );\n\n    Some((new_width, new_height))\n}\n\n/// Check if an image's dimensions would exceed the available memory budget.\n/// Returns true if the image is too large to decode.\npub fn exceeds_memory_limit(width: u32, height: u32, memory_limit_mb: u64) -> bool {\n    let Some(bytes_needed) = calculate_image_memory(width, height) else {\n        // Overflow in calculation means it definitely exceeds any reasonable limit\n        return true;\n    };\n\n    let max_bytes = memory_limit_mb * MB_TO_BYTES;\n    bytes_needed > max_bytes\n}\n\n/// Check if an image should use GPU tiling for display.\n/// Images larger than the atlas fragment size need to be split into tiles for GPU upload.\npub fn should_use_tiling(width: u32, height: u32) -> bool {\n    width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE\n}\n\n/// Determine if an image should use the dedicated worker for thumbnail generation.\n/// Returns (use_dedicated_worker, effective_max_mb, effective_jobs).\n///\n/// Large images that exceed per-worker memory budget get routed to a dedicated worker\n/// with full memory budget. Smaller images use the normal parallel worker pool.\npub fn should_use_dedicated_worker(\n    width: u32,\n    height: u32,\n    total_budget_mb: u64,\n    parallel_workers: usize,\n) -> (bool, u64, usize) {\n    if width == 0 || height == 0 {\n        log::warn!(\n            \"Invalid image dimensions {}x{}, using normal queue\",\n            width,\n            height\n        );\n        return (false, total_budget_mb, parallel_workers);\n    }\n\n    let Some(bytes_needed) = calculate_image_memory(width, height) else {\n        log::warn!(\n            \"Image dimensions {}x{} overflow memory calculation, using normal queue\",\n            width,\n            height\n        );\n        return (false, total_budget_mb, parallel_workers);\n    };\n\n    let mb_needed = bytes_needed / MB_TO_BYTES;\n    let per_worker_budget_mb = total_budget_mb / parallel_workers as u64;\n\n    if mb_needed > per_worker_budget_mb {\n        log::info!(\n            \"Large image {}x{} needs {}MB (exceeds per-worker {}MB), using dedicated worker\",\n            width,\n            height,\n            mb_needed,\n            per_worker_budget_mb\n        );\n        // Use dedicated worker with full budget\n        (true, total_budget_mb, 1)\n    } else {\n        log::debug!(\n            \"Normal image {}x{} needs {}MB (within per-worker {}MB), using parallel workers\",\n            width,\n            height,\n            mb_needed,\n            per_worker_budget_mb\n        );\n        // Use parallel worker pool with shared budget\n        (false, total_budget_mb, parallel_workers)\n    }\n}\n\n/// Get the dimensions of an image without fully decoding it\npub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {\n    match ImageReader::open(path) {\n        Ok(reader) => match reader.into_dimensions() {\n            Ok((width, height)) => {\n                log::debug!(\n                    \"Image dimensions: {}x{} for {}\",\n                    width,\n                    height,\n                    path.display()\n                );\n                Some((width, height))\n            }\n            Err(e) => {\n                log::warn!(\"Failed to get dimensions for {}: {}\", path.display(), e);\n                None\n            }\n        },\n        Err(e) => {\n            log::warn!(\"Failed to open image reader for {}: {}\", path.display(), e);\n            None\n        }\n    }\n}\n\n/// Calculate the memory required to decode an image in bytes.\n/// Returns None if the calculation overflows.\nfn calculate_image_memory(width: u32, height: u32) -> Option<u64> {\n    let pixels = (width as u64).checked_mul(height as u64)?;\n    pixels.checked_mul(RGBA_BYTES_PER_PIXEL)\n}\n\n/// Check if there's sufficient system RAM to decode an image (Linux only).\n/// Returns: (has_memory, error_message)\n#[cfg(target_os = \"linux\")]\nfn check_ram_available(width: u32, height: u32) -> (bool, Option<String>) {\n    use procfs::Current;\n\n    let Some(bytes_needed) = calculate_image_memory(width, height) else {\n        let error_msg = format!(\n            \"Image dimensions too large: {}x{} causes overflow in memory calculation\",\n            width, height\n        );\n        log::error!(\"{}\", error_msg);\n        return (false, Some(error_msg));\n    };\n\n    let mb_needed = bytes_needed / MB_TO_BYTES;\n\n    match procfs::Meminfo::current() {\n        Ok(meminfo) => {\n            // MemAvailable includes reclaimable cache and is the best estimate of\n            // actually available memory for new allocations\n            let available_kb = meminfo.mem_available.unwrap_or(0);\n            let available_bytes = available_kb * 1024;\n\n            // Maintain system reserve to prevent thrashing and OOM killer\n            let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES;\n            let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes);\n\n            if bytes_needed > usable_bytes {\n                let available_mb = available_bytes / MB_TO_BYTES;\n                let error_msg = format!(\n                    \"Insufficient memory: need {}MB, available {}MB. Try closing other applications.\",\n                    mb_needed, available_mb\n                );\n                log::warn!(\"{}\", error_msg);\n                return (false, Some(error_msg));\n            }\n\n            (true, None)\n        }\n        Err(e) => {\n            log::warn!(\"Failed to read /proc/meminfo: {}. Skipping RAM check.\", e);\n            // Graceful fallback: assume RAM is available\n            (true, None)\n        }\n    }\n}\n\n#[cfg(not(target_os = \"linux\"))]\nfn check_ram_available(_width: u32, _height: u32) -> (bool, Option<String>) {\n    // RAM checking not implemented for this platform\n    (true, None)\n}\n\npub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>) {\n    if width == 0 || height == 0 {\n        let error_msg = format!(\n            \"Invalid image dimensions: {}x{} (zero dimension)\",\n            width, height\n        );\n        log::error!(\"{}\", error_msg);\n        return (false, Some(error_msg));\n    }\n\n    // Check system RAM availability\n    check_ram_available(width, height)\n}\n\n/// Decode a large image asynchronously in a blocking thread pool.\n///\n/// This function is used for gallery mode where full-resolution images need to be loaded.\n/// It uses the full memory budget (GALLERY_MEMORY_LIMIT_MB) since only one image\n/// decodes at a time in gallery mode.\npub async fn decode_large_image(\n    path: PathBuf,\n    target_dimensions: Option<(u32, u32)>,\n) -> Option<(PathBuf, u32, u32, Vec<u8>)> {\n    // Decode image in blocking thread pool (CPU-intensive work should not block)\n    tokio::task::spawn_blocking(move || {\n        log::info!(\"Starting async decode of {}\", path.display());\n\n        // Use ImageReader with explicit memory limits to avoid \"Memory limit exceeded\" errors\n        // Gallery mode uses the full memory budget since only one image decodes at a time\n        match image::ImageReader::open(&path) {\n            Ok(reader) => {\n                match reader.with_guessed_format() {\n                    Ok(mut reader) => {\n                        // Note: image crate uses decimal MB (1000^2), not binary MB (1024^2)\n                        let mut limits = image::Limits::default();\n                        limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES);\n                        reader.limits(limits);\n\n                        match reader.decode() {\n                            Ok(img) => {\n                                let rgba = img.into_rgba8();\n                                let orig_width = rgba.width();\n                                let orig_height = rgba.height();\n\n                                // Resize if target dimensions provided\n                                let (final_img, width, height) = if let Some((target_w, target_h)) = target_dimensions {\n                                    log::info!(\n                                        \"Resizing {}x{} -> {}x{} for memory optimization: {}\",\n                                        orig_width, orig_height, target_w, target_h,\n                                        path.display()\n                                    );\n\n                                    // Use Lanczos3 for high-quality downsampling\n                                    let resized = image::imageops::resize(\n                                        &rgba,\n                                        target_w,\n                                        target_h,\n                                        image::imageops::FilterType::Lanczos3,\n                                    );\n\n                                    let resized_w = resized.width();\n                                    let resized_h = resized.height();\n\n                                    log::info!(\n                                        \"Resize complete: {}x{} image now uses ~{} MB instead of ~{} MB\",\n                                        resized_w, resized_h,\n                                        (resized_w as u64 * resized_h as u64 * 4) / MB_TO_BYTES,\n                                        (orig_width as u64 * orig_height as u64 * 4) / MB_TO_BYTES\n                                    );\n\n                                    (resized, resized_w, resized_h)\n                                } else {\n                                    log::info!(\n                                        \"Decoded {}x{} image at full resolution: {}\",\n                                        orig_width, orig_height,\n                                        path.display()\n                                    );\n                                    (rgba, orig_width, orig_height)\n                                };\n\n                                let pixels = final_img.into_raw();\n                                Some((path, width, height, pixels))\n                            }\n                            Err(e) => {\n                                log::warn!(\"Failed to decode {}: {}\", path.display(), e);\n                                None\n                            }\n                        }\n                    }\n                    Err(e) => {\n                        log::warn!(\"Failed to guess format for {}: {}\", path.display(), e);\n                        None\n                    }\n                }\n            }\n            Err(e) => {\n                log::warn!(\"Failed to open {}: {}\", path.display(), e);\n                None\n            }\n        }\n    })\n    .await\n    .ok()\n    .flatten()\n}\n\n/// Manages state and operations for large image decoding in gallery mode\n#[derive(Debug, Default)]\npub struct LargeImageManager {\n    /// Paths of images currently being decoded\n    decoding_images: HashSet<PathBuf>,\n    /// Cache of decoded image handles\n    decoded_images: HashMap<PathBuf, widget::image::Handle>,\n    /// Display dimensions used for each decoded image (for resize detection)\n    decoded_display_sizes: HashMap<PathBuf, (u32, u32)>,\n    /// Errors encountered during decoding\n    decode_errors: HashMap<PathBuf, String>,\n    /// Generation counter for each decode to support cancellation.\n    /// When a new decode is started for the same path, the generation is incremented.\n    /// Only decodes matching the current generation are accepted when they complete.\n    decode_generations: HashMap<PathBuf, u64>,\n}\n\nimpl LargeImageManager {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn is_decoding(&self, path: &Path) -> bool {\n        self.decoding_images.contains(path)\n    }\n\n    pub fn get_decoded(&self, path: &Path) -> Option<&widget::image::Handle> {\n        self.decoded_images.get(path)\n    }\n\n    pub fn get_error(&self, path: &Path) -> Option<&String> {\n        self.decode_errors.get(path)\n    }\n\n    /// Store a decoded image if the generation matches (not superseded by newer decode).\n    /// Returns true if stored, false if rejected due to generation mismatch.\n    pub fn store_decoded_with_generation(\n        &mut self,\n        path: PathBuf,\n        handle: widget::image::Handle,\n        display_size: Option<(u32, u32)>,\n        generation: u64,\n    ) -> bool {\n        // Check if this decode is still current (not superseded by a newer one)\n        if let Some(&current_gen) = self.decode_generations.get(&path)\n            && generation != current_gen\n        {\n            log::info!(\n                \"Discarding outdated decode for {} (generation {} != current {})\",\n                path.display(),\n                generation,\n                current_gen\n            );\n            return false;\n        }\n\n        log::info!(\n            \"Storing decoded image for {} (generation {})\",\n            path.display(),\n            generation\n        );\n\n        self.decoded_images.insert(path.clone(), handle);\n        if let Some(size) = display_size {\n            self.decoded_display_sizes.insert(path.clone(), size);\n        }\n        self.decoding_images.remove(&path);\n        true\n    }\n\n    pub fn store_error(&mut self, path: PathBuf, error: String) {\n        self.decode_errors.insert(path.clone(), error);\n        self.decoding_images.remove(&path);\n    }\n\n    pub fn clear_error(&mut self, path: &Path) {\n        self.decode_errors.remove(path);\n    }\n\n    pub fn clear_cache(&mut self) {\n        log::info!(\n            \"Clearing {} cached images from large image manager\",\n            self.decoded_images.len()\n        );\n        self.decoded_images.clear();\n    }\n\n    pub fn cache_size(&self) -> usize {\n        self.decoded_images.len()\n    }\n\n    pub fn cache_is_empty(&self) -> bool {\n        self.decoded_images.is_empty()\n    }\n\n    /// Check if an image should be re-decoded due to display size increase.\n    /// Returns true only if the display size has INCREASED by more than 20% in either dimension.\n    /// Does NOT re-decode for smaller sizes (GPU can efficiently downscale).\n    pub fn needs_redecode_for_size(\n        &self,\n        path: &Path,\n        new_display_size: Option<(u32, u32)>,\n    ) -> bool {\n        let Some(new_size) = new_display_size else {\n            return false;\n        };\n\n        let Some(&old_size) = self.decoded_display_sizes.get(path) else {\n            return false;\n        };\n\n        const REDECODE_THRESHOLD: f32 = 0.2;\n\n        let width_increase = (new_size.0 as f32 / old_size.0 as f32) - 1.0;\n        let height_increase = (new_size.1 as f32 / old_size.1 as f32) - 1.0;\n\n        let needs_redecode =\n            width_increase > REDECODE_THRESHOLD || height_increase > REDECODE_THRESHOLD;\n\n        if needs_redecode {\n            log::info!(\n                \"Display size increased significantly for {}: {}x{} -> {}x{} (increase: {:.1}% width, {:.1}% height) - re-decoding at higher resolution\",\n                path.display(),\n                old_size.0,\n                old_size.1,\n                new_size.0,\n                new_size.1,\n                width_increase * 100.0,\n                height_increase * 100.0\n            );\n        } else if width_increase < -REDECODE_THRESHOLD || height_increase < -REDECODE_THRESHOLD {\n            log::debug!(\n                \"Display size decreased for {}: {}x{} -> {}x{} (decrease: {:.1}% width, {:.1}% height) - keeping existing higher resolution\",\n                path.display(),\n                old_size.0,\n                old_size.1,\n                new_size.0,\n                new_size.1,\n                width_increase * 100.0,\n                height_increase * 100.0\n            );\n        }\n\n        needs_redecode\n    }\n\n    /// Attempt to decode a large image, checking memory availability first.\n    /// Returns (should_decode, target_dimensions, generation) tuple.\n    pub fn try_decode(\n        &mut self,\n        path: &PathBuf,\n        display_dimensions: Option<(u32, u32)>,\n    ) -> (bool, Option<(u32, u32)>, u64) {\n        self.clear_error(path);\n        let is_currently_decoding = self.is_decoding(path);\n        let needs_redecode = self.needs_redecode_for_size(path, display_dimensions);\n\n        if is_currently_decoding && !needs_redecode {\n            // Get current generation for the ongoing decode\n            let generation = self.decode_generations.get(path).copied().unwrap_or(0);\n            return (false, None, generation);\n        }\n\n        if self.get_decoded(path).is_some() && !needs_redecode && !is_currently_decoding {\n            let generation = self.decode_generations.get(path).copied().unwrap_or(0);\n            return (false, None, generation);\n        }\n\n        let Some((width, height)) = get_image_dimensions(path) else {\n            self.store_error(path.clone(), \"Failed to read image dimensions\".to_string());\n            return (false, None, 0);\n        };\n\n        let target_dimensions = if let Some((display_w, display_h)) = display_dimensions {\n            calculate_target_dimensions(width, height, display_w, display_h)\n        } else {\n            None\n        };\n\n        // Check memory for target size (if resizing) or full size\n        let (check_w, check_h) = target_dimensions.unwrap_or((width, height));\n        if !self.ensure_memory_available(path, check_w, check_h) {\n            return (false, None, 0);\n        }\n\n        // Increment generation counter (cancels any previous decode)\n        let generation = self\n            .decode_generations\n            .entry(path.clone())\n            .and_modify(|g| *g += 1)\n            .or_insert(1);\n        let generation = *generation;\n\n        if is_currently_decoding {\n            log::info!(\n                \"Cancelling previous decode for {} and starting new one (generation {})\",\n                path.display(),\n                generation\n            );\n        }\n\n        // Mark as decoding\n        self.decoding_images.insert(path.clone());\n        (true, target_dimensions, generation)\n    }\n\n    /// Check if sufficient memory is available, clearing cache if needed.\n    /// Returns true if memory is available, false otherwise.\n    fn ensure_memory_available(&mut self, path: &Path, width: u32, height: u32) -> bool {\n        let (has_memory, error_opt) = check_memory_available(width, height);\n\n        if has_memory {\n            return true;\n        }\n\n        if self.cache_is_empty() {\n            if let Some(error_msg) = error_opt {\n                self.store_error(path.to_path_buf(), error_msg);\n                log::warn!(\n                    \"Cannot load {}: insufficient memory and cache is empty\",\n                    path.display()\n                );\n            }\n            return false;\n        }\n\n        log::info!(\n            \"Insufficient memory, clearing {} cached images\",\n            self.cache_size()\n        );\n        self.clear_cache();\n\n        let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height);\n\n        if has_memory_after_clear {\n            log::info!(\"Memory available after cache clear, proceeding with decode\");\n            return true;\n        }\n\n        if let Some(error_msg) = error_opt_after {\n            self.store_error(path.to_path_buf(), error_msg);\n            log::warn!(\n                \"Cannot load {}: insufficient memory even after cache clear\",\n                path.display()\n            );\n        }\n        false\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::app::Settings;\nuse cosmic::iced::Limits;\nuse std::path::PathBuf;\nuse std::{env, fs, process};\nuse tracing_subscriber::layer::SubscriberExt;\nuse tracing_subscriber::util::SubscriberInitExt;\n\nuse crate::app::{App, Flags};\nuse crate::config::{Config, State};\nuse crate::tab::Location;\n\npub mod app;\nmod archive;\npub mod channel;\npub mod clipboard;\npub mod config;\nmod context_action;\npub mod dialog;\nmod key_bind;\npub(crate) mod large_image;\npub(crate) mod load_image;\nmod localize;\nmod menu;\nmod mime_app;\npub mod mime_icon;\nmod mounter;\nmod mouse_area;\npub mod operation;\nmod spawn_detached;\npub mod tab;\nmod thumbnail_cacher;\nmod thumbnailer;\npub(crate) mod trash;\nmod zoom;\n\npub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;\n\npub(crate) fn err_str<T: ToString>(err: T) -> String {\n    err.to_string()\n}\n\npub fn desktop_dir() -> PathBuf {\n    if let Some(path) = dirs::desktop_dir() {\n        path\n    } else {\n        let path = home_dir().join(\"Desktop\");\n        log::warn!(\n            \"failed to locate desktop directory, falling back to {}\",\n            path.display()\n        );\n        path\n    }\n}\n\npub fn home_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home\n    } else {\n        let path = PathBuf::from(\"/\");\n        log::warn!(\n            \"failed to locate home directory, falling back to {}\",\n            path.display()\n        );\n        path\n    }\n}\n\npub fn is_wayland() -> bool {\n    matches!(\n        cosmic::app::cosmic::windowing_system(),\n        Some(cosmic::app::cosmic::WindowingSystem::Wayland)\n    )\n}\n\n/// Runs application in desktop mode\n#[rustfmt::skip]\npub fn desktop() -> Result<(), Box<dyn std::error::Error>> {\n    let log_format = tracing_subscriber::fmt::format()\n        .pretty()\n        .without_time()\n        .with_line_number(true)\n        .with_file(true)\n        .with_target(false)\n        .with_thread_names(true);\n\n    let log_layer = tracing_subscriber::fmt::Layer::default()\n        .with_writer(std::io::stderr)\n        .event_format(log_format);\n\n    tracing_subscriber::registry()\n        .with(tracing_subscriber::EnvFilter::from_env(\"RUST_LOG\"))\n        .with(log_layer)\n        .init();\n\n    localize::localize();\n\n    let (config_handler, config) = Config::load();\n    let (state_handler, state) = State::load();\n\n    let mut settings = Settings::default();\n    settings = settings.theme(config.app_theme.theme());\n    settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));\n    settings = settings.exit_on_close(false);\n    settings = settings.transparent(true);\n    #[cfg(all(feature = \"wayland\", feature = \"desktop-applet\"))]\n    {\n        settings = settings.no_main_window(true);\n    }\n\n    let locations = vec![tab::Location::Desktop(desktop_dir(), String::new(), config.desktop)];\n    let flags = Flags {\n        config_handler,\n        config,\n        state_handler,\n        state,\n        mode: app::Mode::Desktop,\n        locations,\n        uris: Vec::new()\n    };\n    cosmic::app::run::<App>(settings, flags)?;\n\n    Ok(())\n}\n\n/// Runs application with these settings\n#[rustfmt::skip]\npub fn main() -> Result<(), Box<dyn std::error::Error>> {\n    let log_format = tracing_subscriber::fmt::format()\n        .pretty()\n        .with_line_number(true)\n        .with_file(true)\n        .with_target(false)\n        .with_thread_names(true);\n\n    let log_layer = tracing_subscriber::fmt::Layer::default()\n        .with_writer(std::io::stderr)\n        .event_format(log_format);\n\n    tracing_subscriber::registry()\n        .with(tracing_subscriber::EnvFilter::from_default_env())\n        .with(log_layer)\n        .init();\n\n    localize::localize();\n\n    let (config_handler, config) = Config::load();\n    let (state_handler, state) = State::load();\n\n    let mut daemonize = true;\n    let mut locations = Vec::new();\n    let mut uris = Vec::new();\n    for arg in env::args().skip(1) {\n        let location = if &arg == \"--no-daemon\" {\n            daemonize = false;\n            continue;\n        } else if &arg == \"--trash\" {\n            Location::Trash\n        } else if &arg == \"--recents\" {\n            if config.show_recents {\n                Location::Recents\n            } else {\n                log::warn!(\"recents feature is disabled in config\");\n                continue;\n            }\n        } else if &arg == \"--network\" {\n            Location::Network(\"network:///\".to_string(), fl!(\"networks\"), None)\n        } else {\n            //TODO: support more URLs\n            let path = match url::Url::parse(&arg) {\n                Ok(url) if url.scheme() == \"file\" => if let Ok(path) = url.to_file_path() { path } else {\n                    log::warn!(\"invalid argument {arg:?}\");\n                    continue;\n                },\n                Ok(url) => {\n                    uris.push(url);\n                    continue;\n                }\n                _ => PathBuf::from(arg),\n            };\n            match fs::canonicalize(&path) {\n                Ok(absolute) => Location::Path(absolute),\n                Err(err) => {\n                    log::warn!(\"failed to canonicalize {}: {}\", path.display(), err);\n                    continue;\n                }\n            }\n        };\n        locations.push(location);\n    }\n\n    if daemonize {\n        #[cfg(all(unix, not(any(target_os = \"macos\", target_os = \"redox\"))))]\n        match fork::daemon(true, true) {\n            Ok(fork::Fork::Child) => (),\n            Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),\n            Err(err) => {\n                eprintln!(\"failed to daemonize: {err:?}\");\n                process::exit(1);\n            }\n        }\n    }\n\n    let mut settings = Settings::default();\n    settings = settings.theme(config.app_theme.theme());\n    settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));\n    settings = settings.exit_on_close(false);\n\n    #[cfg(feature = \"jemalloc\")]\n    {\n        settings = settings.default_mmap_threshold(None);\n    }\n\n    let flags = Flags {\n        config_handler,\n        config,\n        state_handler,\n        state,\n        mode: app::Mode::App,\n        locations,\n        uris\n    };\n    cosmic::app::run::<App>(settings, flags)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/load_image.rs",
    "content": "use cosmic::iced::{core as iced_core, widget as iced_widget};\nuse iced_core::event::Event;\nuse iced_core::widget::{Operation, Tree};\nuse iced_core::{\n    Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay,\n    renderer,\n};\n\npub fn loaded_image<'a, Message: 'static, Theme>(\n    handle: <cosmic::Renderer as iced_core::image::Renderer>::Handle,\n) -> LoadedImage<'a, Message, Theme, cosmic::Renderer>\nwhere\n    Theme: iced_widget::container::Catalog,\n    <Theme as iced_widget::container::Catalog>::Class<'a>: From<cosmic::theme::Container<'a>>,\n{\n    LoadedImage::new(handle)\n}\n\n/// Forces the wrapped image to be loaded before drawing.\n///\n/// May cause a dropped frame if the image is not already in the cache.\n/// This is useful when you want to ensure an image is loaded before it is drawn, for example when swapping out a placeholder.\n/// Otherwise, the image may be blank until the next redraw.\n#[allow(missing_debug_implementations)]\npub struct LoadedImage<'a, Message, Theme, Renderer>\nwhere\n    Renderer: iced_core::Renderer + iced_core::image::Renderer,\n{\n    handle: <Renderer as iced_core::image::Renderer>::Handle,\n    content: cosmic::iced::Element<'a, Message, Theme, Renderer>,\n}\n\nimpl<'a, Message, Theme, Renderer> LoadedImage<'a, Message, Theme, Renderer>\nwhere\n    Renderer: iced_core::Renderer + iced_core::image::Renderer,\n    <Renderer as iced_core::image::Renderer>::Handle: 'a,\n{\n    /// Creates an empty [`LoadedImage`].\n    pub(crate) fn new(handle: <Renderer as iced_core::image::Renderer>::Handle) -> Self {\n        LoadedImage {\n            handle: handle.clone(),\n            content: cosmic::widget::Image::new(handle).into(),\n        }\n    }\n}\n\nimpl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>\n    for LoadedImage<'_, Message, Theme, Renderer>\nwhere\n    Renderer: iced_core::Renderer + iced_core::image::Renderer,\n{\n    fn children(&self) -> Vec<Tree> {\n        vec![Tree::new(&self.content)]\n    }\n\n    fn diff(&mut self, tree: &mut Tree) {\n        tree.diff_children(std::slice::from_mut(&mut self.content));\n    }\n\n    fn size(&self) -> iced_core::Size<Length> {\n        self.content.as_widget().size()\n    }\n\n    fn layout(\n        &mut self,\n        tree: &mut Tree,\n        renderer: &Renderer,\n        limits: &layout::Limits,\n    ) -> layout::Node {\n        let node = self\n            .content\n            .as_widget_mut()\n            .layout(&mut tree.children[0], renderer, limits);\n        let size = node.size();\n        layout::Node::with_children(size, vec![node])\n    }\n\n    fn operate(\n        &mut self,\n        tree: &mut Tree,\n        layout: Layout<'_>,\n        renderer: &Renderer,\n        operation: &mut dyn Operation,\n    ) {\n        operation.container(None, layout.bounds());\n        operation.traverse(&mut |operation| {\n            self.content.as_widget_mut().operate(\n                &mut tree.children[0],\n                layout\n                    .children()\n                    .next()\n                    .unwrap()\n                    .with_virtual_offset(layout.virtual_offset()),\n                renderer,\n                operation,\n            );\n        });\n    }\n\n    fn update(\n        &mut self,\n        tree: &mut Tree,\n        event: &Event,\n        layout: Layout<'_>,\n        cursor_position: mouse::Cursor,\n        renderer: &Renderer,\n        clipboard: &mut dyn Clipboard,\n        shell: &mut Shell<'_, Message>,\n        viewport: &Rectangle,\n    ) {\n        self.content.as_widget_mut().update(\n            &mut tree.children[0],\n            event,\n            layout\n                .children()\n                .next()\n                .unwrap()\n                .with_virtual_offset(layout.virtual_offset()),\n            cursor_position,\n            renderer,\n            clipboard,\n            shell,\n            viewport,\n        );\n    }\n\n    fn mouse_interaction(\n        &self,\n        tree: &Tree,\n        layout: Layout<'_>,\n        cursor_position: mouse::Cursor,\n        viewport: &Rectangle,\n        renderer: &Renderer,\n    ) -> mouse::Interaction {\n        let content_layout = layout.children().next().unwrap();\n        self.content.as_widget().mouse_interaction(\n            &tree.children[0],\n            content_layout.with_virtual_offset(layout.virtual_offset()),\n            cursor_position,\n            viewport,\n            renderer,\n        )\n    }\n\n    fn draw(\n        &self,\n        tree: &Tree,\n        renderer: &mut Renderer,\n        theme: &Theme,\n        renderer_style: &renderer::Style,\n        layout: Layout<'_>,\n        cursor_position: mouse::Cursor,\n        viewport: &Rectangle,\n    ) {\n        let content_layout = layout.children().next().unwrap();\n\n        // forces image to be loaded before drawing\n        _ = renderer.load_image(&self.handle);\n        self.content.as_widget().draw(\n            &tree.children[0],\n            renderer,\n            theme,\n            renderer_style,\n            content_layout.with_virtual_offset(layout.virtual_offset()),\n            cursor_position,\n            viewport,\n        );\n    }\n\n    fn overlay<'b>(\n        &'b mut self,\n        tree: &'b mut Tree,\n        layout: Layout<'b>,\n        renderer: &Renderer,\n        viewport: &Rectangle,\n        translation: Vector,\n    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {\n        self.content.as_widget_mut().overlay(\n            &mut tree.children[0],\n            layout\n                .children()\n                .next()\n                .unwrap()\n                .with_virtual_offset(layout.virtual_offset()),\n            renderer,\n            viewport,\n            translation,\n        )\n    }\n\n    fn drag_destinations(\n        &self,\n        state: &Tree,\n        layout: Layout<'_>,\n        renderer: &Renderer,\n        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,\n    ) {\n        let content_layout = layout.children().next().unwrap();\n        self.content.as_widget().drag_destinations(\n            &state.children[0],\n            content_layout.with_virtual_offset(layout.virtual_offset()),\n            renderer,\n            dnd_rectangles,\n        );\n    }\n}\n\nimpl<'a, Message, Theme, Renderer> From<LoadedImage<'a, Message, Theme, Renderer>>\n    for Element<'a, Message, Theme, Renderer>\nwhere\n    Message: 'a,\n    Renderer: 'a + iced_core::Renderer + iced_core::image::Renderer,\n    Theme: 'a,\n{\n    fn from(c: LoadedImage<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> {\n        Element::new(c)\n    }\n}\n"
  },
  {
    "path": "src/localize.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n\nuse i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};\nuse i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};\nuse icu::collator::options::CollatorOptions;\nuse icu::collator::preferences::CollationNumericOrdering;\nuse icu::collator::{Collator, CollatorBorrowed, CollatorPreferences};\nuse icu::locale::Locale;\nuse rust_embed::RustEmbed;\nuse std::sync::LazyLock;\n\n#[derive(RustEmbed)]\n#[folder = \"i18n/\"]\nstruct Localizations;\n\npub static LANGUAGE_LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {\n    let loader: FluentLanguageLoader = fluent_language_loader!();\n\n    loader\n        .load_fallback_language(&Localizations)\n        .expect(\"Error while loading fallback language\");\n\n    loader\n});\n\npub static LANGUAGE_SORTER: LazyLock<CollatorBorrowed> = LazyLock::new(|| {\n    let create_collator = |locale: Locale| {\n        let mut prefs = CollatorPreferences::from(locale);\n        prefs.numeric_ordering = Some(CollationNumericOrdering::True);\n        Collator::try_new(prefs, CollatorOptions::default()).ok()\n    };\n\n    Locale::try_from_str(&LANGUAGE_LOADER.current_language().to_string())\n            .ok()\n            .and_then(create_collator)\n            .or_else(|| {\n                Locale::try_from_str(&LANGUAGE_LOADER.fallback_language().to_string())\n                    .ok()\n                    .and_then(create_collator)\n            })\n            .unwrap_or_else(|| {\n                let locale = Locale::try_from_str(\"en-US\").expect(\"en-US is a valid BCP-47 tag\");\n                create_collator(locale)\n                    .expect(\"Creating a collator from the system's current language, the fallback language, or American English should succeed\")\n            })\n});\n\npub static LOCALE: LazyLock<Locale> = LazyLock::new(|| {\n    for var in [\"LC_TIME\", \"LC_ALL\", \"LANG\"] {\n        if let Ok(locale_str) = std::env::var(var) {\n            let cleaned_locale = locale_str\n                .split('.')\n                .next()\n                .unwrap_or(&locale_str)\n                .replace('_', \"-\");\n\n            if let Ok(locale) = Locale::try_from_str(&cleaned_locale) {\n                return locale;\n            }\n\n            // Try language-only fallback (e.g., \"en\" from \"en-US\")\n            if let Some(lang) = cleaned_locale.split('-').next()\n                && let Ok(locale) = Locale::try_from_str(lang)\n            {\n                return locale;\n            }\n        }\n    }\n    log::warn!(\"No valid locale found in environment, using fallback\");\n    Locale::try_from_str(\"en-US\").expect(\"Failed to parse fallback locale 'en-US'\")\n});\n\n#[macro_export]\nmacro_rules! fl {\n    ($message_id:literal) => {{\n        i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)\n    }};\n\n    ($message_id:literal, $($args:expr),*) => {{\n        i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)\n    }};\n}\n\n// Get the `Localizer` to be used for localizing this library.\npub fn localizer() -> Box<dyn Localizer> {\n    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))\n}\n\npub fn localize() {\n    let localizer = localizer();\n    let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();\n\n    if let Err(error) = localizer.select(&requested_languages) {\n        eprintln!(\"Error while loading language for COSMIC Files {error}\");\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#[cfg(feature = \"jemalloc\")]\nuse tikv_jemallocator::Jemalloc;\n\n#[cfg(feature = \"jemalloc\")]\n#[global_allocator]\nstatic GLOBAL: Jemalloc = Jemalloc;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let _ = jxl_oxide::integration::register_image_decoding_hook();\n    cosmic_files::main()\n}\n"
  },
  {
    "path": "src/menu.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::app::Core;\nuse cosmic::iced::advanced::widget::text::Style as TextStyle;\nuse cosmic::iced::keyboard::Modifiers;\nuse cosmic::iced::{Alignment, Background, Border, Length};\nuse cosmic::widget::menu::key_bind::KeyBind;\nuse cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};\nuse cosmic::widget::{\n    self, Row, button, column, container, divider, responsive_menu_bar, space, text,\n};\nuse cosmic::{Element, theme};\nuse i18n_embed::LanguageLoader;\nuse mime_guess::Mime;\nuse std::collections::HashMap;\nuse std::sync::LazyLock;\n\nuse crate::app::{Action, Message};\nuse crate::config::{Config, ContextActionPreset};\nuse crate::fl;\nuse crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab};\nuse crate::trash::{Trash, TrashExt};\n\nstatic MENU_ID: LazyLock<cosmic::widget::Id> =\n    LazyLock::new(|| cosmic::widget::Id::new(\"responsive-menu\"));\n\nmacro_rules! menu_button {\n    ($($x:expr),+ $(,)?) => (\n        button::custom(\n            Row::with_children(\n                [$(Element::from($x)),+]\n            )\n            .height(Length::Fixed(24.0))\n            .align_y(Alignment::Center)\n        )\n        .padding([theme::spacing().space_xxs, 16])\n        .width(Length::Fill)\n        .class(theme::Button::MenuItem)\n    );\n}\n\nconst fn menu_button_optional(\n    label: String,\n    action: Action,\n    enabled: bool,\n) -> menu::Item<Action, String> {\n    if enabled {\n        menu::Item::Button(label, None, action)\n    } else {\n        menu::Item::ButtonDisabled(label, None, action)\n    }\n}\n\npub fn context_menu<'a>(\n    tab: &Tab,\n    key_binds: &HashMap<KeyBind, Action>,\n    modifiers: &Modifiers,\n    clipboard_paste_available: bool,\n    context_actions: &[ContextActionPreset],\n) -> Element<'a, tab::Message> {\n    let find_key = |action: &Action| -> String {\n        for (key_bind, key_action) in key_binds {\n            if action == key_action {\n                return key_bind.to_string();\n            }\n        }\n        String::new()\n    };\n    fn key_style(theme: &cosmic::Theme) -> TextStyle {\n        let mut color = theme.cosmic().background.component.on;\n        color.alpha *= 0.75;\n        TextStyle {\n            color: Some(color.into()),\n        }\n    }\n    fn disabled_style(theme: &cosmic::Theme) -> TextStyle {\n        let mut color = theme.cosmic().background.component.on;\n        color.alpha *= 0.5;\n        TextStyle {\n            color: Some(color.into()),\n        }\n    }\n\n    let menu_item = |label, action| {\n        let key = find_key(&action);\n        menu_button!(\n            text::body(label),\n            space::horizontal(),\n            text::body(key).class(theme::Text::Custom(key_style))\n        )\n        .on_press(tab::Message::ContextAction(action))\n    };\n\n    let menu_item_disabled = |label, action: Action| {\n        let key = find_key(&action);\n        menu_button!(\n            text::body(label).class(theme::Text::Custom(disabled_style)),\n            space::horizontal(),\n            text::body(key).class(theme::Text::Custom(disabled_style))\n        )\n    };\n\n    // Allow paste when clipboard has data and we're in a location that supports it\n    let can_paste = clipboard_paste_available && tab.location.supports_paste();\n\n    let (sort_name, sort_direction, _) = tab.sort_options();\n    let sort_item = |label, variant| {\n        menu_item(\n            format!(\n                \"{} {}\",\n                label,\n                match (sort_name == variant, sort_direction) {\n                    (true, true) => \"\\u{2B07}\",\n                    (true, false) => \"\\u{2B06}\",\n                    _ => \"\",\n                }\n            ),\n            Action::ToggleSort(variant),\n        )\n        .into()\n    };\n\n    let mut selected_dir = 0;\n    let mut selected = 0;\n    let mut selected_trash_only = false;\n    let mut selected_desktop_entry = None;\n    let mut selected_types: Vec<Mime> = vec![];\n    let mut selected_mount_point = 0;\n    if let Some(items) = tab.items_opt() {\n        for item in items {\n            if item.selected {\n                selected += 1;\n                if item.metadata.is_dir() {\n                    selected_mount_point += i32::from(item.is_mount_point);\n                    selected_dir += 1;\n                }\n                match &item.location_opt {\n                    Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => {\n                        selected_trash_only = true\n                    }\n                    Some(Location::Path(path))\n                        if selected == 1\n                            && path.extension().and_then(|s| s.to_str()) == Some(\"desktop\") =>\n                    {\n                        selected_desktop_entry = Some(&**path);\n                    }\n                    _ => (),\n                }\n                selected_types.push(item.mime.clone());\n            }\n        }\n    }\n    selected_types.sort_unstable();\n    selected_types.dedup();\n    selected_trash_only = selected_trash_only && selected == 1;\n    let context_action_items = |selected: usize, selected_dir: usize| {\n        context_actions\n            .iter()\n            .enumerate()\n            .filter(|(_, action)| action.matches_selection(selected, selected_dir))\n            .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into())\n            .collect::<Vec<Element<'a, tab::Message>>>()\n    };\n    // Parse the desktop entry if it is the only selection\n    #[cfg(feature = \"desktop\")]\n    let selected_desktop_entry = selected_desktop_entry.and_then(|path| {\n        if selected == 1 {\n            let lang_id = crate::localize::LANGUAGE_LOADER.current_language();\n            let language = lang_id.language.as_str();\n            // Cache?\n            cosmic::desktop::load_desktop_file(&[language.into()], path.into())\n        } else {\n            None\n        }\n    });\n\n    let mut children: Vec<Element<_>> = Vec::new();\n    match (&tab.mode, &tab.location) {\n        (\n            tab::Mode::App | tab::Mode::Desktop,\n            Location::Desktop(..)\n            | Location::Path(..)\n            | Location::Search(SearchLocation::Path(..), ..)\n            | Location::Search(SearchLocation::Recents, ..)\n            | Location::Recents\n            | Location::Network(_, _, Some(_)),\n        ) => {\n            if selected_trash_only {\n                children.push(menu_item(fl!(\"open\"), Action::Open).into());\n                if !Trash::is_empty() {\n                    children.push(menu_item(fl!(\"empty-trash\"), Action::EmptyTrash).into());\n                }\n            } else if let Some(entry) = selected_desktop_entry {\n                children.push(menu_item(fl!(\"open\"), Action::Open).into());\n                #[cfg(feature = \"desktop\")]\n                {\n                    children.extend(entry.desktop_actions.into_iter().enumerate().map(\n                        |(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),\n                    ));\n                }\n                children.push(divider::horizontal::light().into());\n                children.push(menu_item(fl!(\"rename\"), Action::Rename).into());\n                children.push(menu_item(fl!(\"cut\"), Action::Cut).into());\n                if modifiers.shift() && !modifiers.control() {\n                    children.push(menu_item(fl!(\"copy-path\"), Action::CopyPath).into());\n                } else {\n                    children.push(menu_item(fl!(\"copy\"), Action::Copy).into());\n                }\n                // Should this simply bypass trash and remove the shortcut?\n                children.push(menu_item(fl!(\"move-to-trash\"), Action::Delete).into());\n                let action_items = context_action_items(selected, selected_dir);\n                if !action_items.is_empty() {\n                    children.push(divider::horizontal::light().into());\n                    children.extend(action_items);\n                }\n            } else if selected > 0 {\n                if selected_dir == 1 && selected == 1 || selected_dir == 0 {\n                    children.push(menu_item(fl!(\"open\"), Action::Open).into());\n                }\n                if selected == 1 {\n                    children.push(menu_item(fl!(\"menu-open-with\"), Action::OpenWith).into());\n                    if selected_dir == 1 {\n                        children\n                            .push(menu_item(fl!(\"open-in-terminal\"), Action::OpenTerminal).into());\n                    }\n                }\n                if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) {\n                    children.push(\n                        menu_item(fl!(\"open-item-location\"), Action::OpenItemLocation).into(),\n                    );\n                }\n                // All selected items are directories\n                if selected == selected_dir && matches!(tab.mode, tab::Mode::App) {\n                    children.push(menu_item(fl!(\"open-in-new-tab\"), Action::OpenInNewTab).into());\n                    children\n                        .push(menu_item(fl!(\"open-in-new-window\"), Action::OpenInNewWindow).into());\n                }\n                let action_items = context_action_items(selected, selected_dir);\n                if !action_items.is_empty() {\n                    children.push(divider::horizontal::light().into());\n                    children.extend(action_items);\n                }\n                children.push(divider::horizontal::light().into());\n                if selected_mount_point == 0 {\n                    children.push(menu_item(fl!(\"rename\"), Action::Rename).into());\n                    children.push(menu_item(fl!(\"cut\"), Action::Cut).into());\n                }\n                if modifiers.shift() && !modifiers.control() {\n                    children.push(menu_item(fl!(\"copy-path\"), Action::CopyPath).into());\n                } else {\n                    children.push(menu_item(fl!(\"copy\"), Action::Copy).into());\n                }\n                if selected_mount_point == 0 {\n                    children.push(menu_item(fl!(\"move-to\"), Action::MoveTo).into());\n                }\n                children.push(menu_item(fl!(\"copy-to\"), Action::CopyTo).into());\n\n                children.push(divider::horizontal::light().into());\n                let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;\n                selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));\n                if selected_types.is_empty() {\n                    children.push(menu_item(fl!(\"extract-here\"), Action::ExtractHere).into());\n                    children.push(menu_item(fl!(\"extract-to\"), Action::ExtractTo).into());\n                }\n                children.push(menu_item(fl!(\"compress\"), Action::Compress).into());\n                children.push(divider::horizontal::light().into());\n\n                //TODO: Print?\n                children.push(menu_item(fl!(\"show-details\"), Action::Preview).into());\n                if matches!(tab.mode, tab::Mode::App) {\n                    children.push(divider::horizontal::light().into());\n                    children.push(menu_item(fl!(\"add-to-sidebar\"), Action::AddToSidebar).into());\n                }\n                children.push(divider::horizontal::light().into());\n                if tab.location.is_recents() {\n                    children.push(\n                        menu_item(fl!(\"remove-from-recents\"), Action::RemoveFromRecents).into(),\n                    );\n                    children.push(divider::horizontal::light().into());\n                }\n                if selected_mount_point == 0 {\n                    if modifiers.shift() && !modifiers.control() {\n                        children.push(\n                            menu_item(fl!(\"delete-permanently\"), Action::PermanentlyDelete).into(),\n                        );\n                    } else {\n                        children.push(menu_item(fl!(\"move-to-trash\"), Action::Delete).into());\n                    }\n                } else if selected == 1 {\n                    children.push(menu_item(fl!(\"eject\"), Action::Eject).into());\n                }\n            } else {\n                //TODO: need better designs for menu with no selection\n                //TODO: have things like properties but they apply to the folder?\n                if tab.location != Location::Recents {\n                    children.push(menu_item(fl!(\"new-folder\"), Action::NewFolder).into());\n                    children.push(menu_item(fl!(\"new-file\"), Action::NewFile).into());\n                    children.push(menu_item(fl!(\"open-in-terminal\"), Action::OpenTerminal).into());\n                    children.push(divider::horizontal::light().into());\n                }\n\n                if tab.mode.multiple() {\n                    children.push(menu_item(fl!(\"select-all\"), Action::SelectAll).into());\n                }\n                if can_paste {\n                    children.push(menu_item(fl!(\"paste\"), Action::Paste).into());\n                } else {\n                    children.push(menu_item_disabled(fl!(\"paste\"), Action::Paste).into());\n                }\n\n                //TODO: only show if cosmic-settings is found?\n                if matches!(tab.mode, tab::Mode::Desktop) {\n                    children.push(divider::horizontal::light().into());\n                    children.push(\n                        menu_item(fl!(\"change-wallpaper\"), Action::CosmicSettingsWallpaper).into(),\n                    );\n                    children.push(\n                        menu_item(fl!(\"desktop-appearance\"), Action::CosmicSettingsDesktop).into(),\n                    );\n                    children.push(\n                        menu_item(fl!(\"display-settings\"), Action::CosmicSettingsDisplays).into(),\n                    );\n                }\n\n                children.push(divider::horizontal::light().into());\n                // TODO: Nested menu\n                children.push(sort_item(fl!(\"sort-by-name\"), HeadingOptions::Name));\n                children.push(sort_item(fl!(\"sort-by-modified\"), HeadingOptions::Modified));\n                children.push(sort_item(fl!(\"sort-by-size\"), HeadingOptions::Size));\n                if matches!(tab.location, Location::Desktop(..)) {\n                    children.push(divider::horizontal::light().into());\n                    children.push(\n                        menu_item(fl!(\"desktop-view-options\"), Action::DesktopViewOptions).into(),\n                    );\n                }\n            }\n        }\n        (\n            tab::Mode::Dialog(dialog_kind),\n            Location::Desktop(..)\n            | Location::Path(..)\n            | Location::Search(SearchLocation::Path(..), ..)\n            | Location::Search(SearchLocation::Recents, ..)\n            | Location::Recents\n            | Location::Network(_, _, Some(_)),\n        ) => {\n            if selected > 0 {\n                if selected_dir == 1 && selected == 1 || selected_dir == 0 {\n                    children.push(menu_item(fl!(\"open\"), Action::Open).into());\n                }\n                if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() {\n                    children.push(\n                        menu_item(fl!(\"open-item-location\"), Action::OpenItemLocation).into(),\n                    );\n                }\n                children.push(divider::horizontal::light().into());\n                children.push(menu_item(fl!(\"show-details\"), Action::Preview).into());\n            } else {\n                if dialog_kind.save() {\n                    children.push(menu_item(fl!(\"new-folder\"), Action::NewFolder).into());\n                }\n                if tab.mode.multiple() {\n                    children.push(menu_item(fl!(\"select-all\"), Action::SelectAll).into());\n                }\n                if !children.is_empty() {\n                    children.push(divider::horizontal::light().into());\n                }\n                children.push(sort_item(fl!(\"sort-by-name\"), HeadingOptions::Name));\n                children.push(sort_item(fl!(\"sort-by-modified\"), HeadingOptions::Modified));\n                children.push(sort_item(fl!(\"sort-by-size\"), HeadingOptions::Size));\n            }\n        }\n        (_, Location::Network(..)) => {\n            if selected > 0 {\n                if selected_dir == 1 && selected == 1 || selected_dir == 0 {\n                    children.push(menu_item(fl!(\"open\"), Action::Open).into());\n                }\n            } else {\n                if tab.mode.multiple() {\n                    children.push(menu_item(fl!(\"select-all\"), Action::SelectAll).into());\n                }\n                if !children.is_empty() {\n                    children.push(divider::horizontal::light().into());\n                }\n                children.push(sort_item(fl!(\"sort-by-name\"), HeadingOptions::Name));\n                children.push(sort_item(fl!(\"sort-by-modified\"), HeadingOptions::Modified));\n                children.push(sort_item(fl!(\"sort-by-size\"), HeadingOptions::Size));\n            }\n        }\n        (_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {\n            if tab.mode.multiple() {\n                children.push(menu_item(fl!(\"select-all\"), Action::SelectAll).into());\n            }\n            if !children.is_empty() {\n                children.push(divider::horizontal::light().into());\n            }\n            if selected > 0 {\n                children.push(menu_item(fl!(\"show-details\"), Action::Preview).into());\n                children.push(divider::horizontal::light().into());\n                children\n                    .push(menu_item(fl!(\"restore-from-trash\"), Action::RestoreFromTrash).into());\n                children.push(divider::horizontal::light().into());\n                children.push(menu_item(fl!(\"delete-permanently\"), Action::Delete).into());\n            } else {\n                // TODO: Nested menu\n                children.push(sort_item(fl!(\"sort-by-name\"), HeadingOptions::Name));\n                children.push(sort_item(fl!(\"sort-by-trashed\"), HeadingOptions::TrashedOn));\n                children.push(sort_item(fl!(\"sort-by-size\"), HeadingOptions::Size));\n            }\n        }\n    }\n\n    container(column::with_children(children))\n        .padding(1)\n        //TODO: move style to libcosmic\n        .style(|theme| {\n            let cosmic = theme.cosmic();\n            let component = &cosmic.background.component;\n            container::Style {\n                icon_color: Some(component.on.into()),\n                text_color: Some(component.on.into()),\n                background: Some(Background::Color(component.base.into())),\n                border: Border {\n                    radius: cosmic.radius_s().map(|x| x + 1.0).into(),\n                    width: 1.0,\n                    color: component.divider.into(),\n                },\n                ..Default::default()\n            }\n        })\n        .width(Length::Fixed(360.0))\n        .into()\n}\n\npub fn dialog_menu(\n    tab: &Tab,\n    key_binds: &HashMap<KeyBind, Action>,\n    show_details: bool,\n) -> Element<'static, Message> {\n    let (sort_name, sort_direction, _) = tab.sort_options();\n    let sort_item = |label, sort, dir| {\n        menu::Item::CheckBox(\n            label,\n            None,\n            sort_name == sort && sort_direction == dir,\n            Action::SetSort(sort, dir),\n        )\n    };\n    let in_trash = tab.location.is_trash();\n\n    let mut selected_gallery = 0;\n    if let Some(items) = tab.items_opt() {\n        for item in items {\n            if item.selected && item.can_gallery() {\n                selected_gallery += 1;\n            }\n        }\n    }\n\n    MenuBar::new(vec![\n        menu::Tree::with_children(\n            Element::from(\n                widget::button::icon(widget::icon::from_name(match tab.config.view {\n                    tab::View::Grid => \"view-grid-symbolic\",\n                    tab::View::List => \"view-list-symbolic\",\n                }))\n                // This prevents the button from being shown as insensitive\n                .on_press(Message::None)\n                .padding(8),\n            ),\n            menu::items(\n                key_binds,\n                vec![\n                    menu::Item::CheckBox(\n                        fl!(\"grid-view\"),\n                        None,\n                        matches!(tab.config.view, tab::View::Grid),\n                        Action::TabViewGrid,\n                    ),\n                    menu::Item::CheckBox(\n                        fl!(\"list-view\"),\n                        None,\n                        matches!(tab.config.view, tab::View::List),\n                        Action::TabViewList,\n                    ),\n                ],\n            ),\n        ),\n        menu::Tree::with_children(\n            Element::from(\n                widget::button::icon(widget::icon::from_name(if sort_direction {\n                    \"view-sort-ascending-symbolic\"\n                } else {\n                    \"view-sort-descending-symbolic\"\n                }))\n                // This prevents the button from being shown as insensitive\n                .on_press(Message::None)\n                .padding(8),\n            ),\n            menu::items(\n                key_binds,\n                vec![\n                    sort_item(fl!(\"sort-a-z\"), tab::HeadingOptions::Name, true),\n                    sort_item(fl!(\"sort-z-a\"), tab::HeadingOptions::Name, false),\n                    sort_item(\n                        fl!(\"sort-newest-first\"),\n                        if in_trash {\n                            tab::HeadingOptions::TrashedOn\n                        } else {\n                            tab::HeadingOptions::Modified\n                        },\n                        false,\n                    ),\n                    sort_item(\n                        fl!(\"sort-oldest-first\"),\n                        if in_trash {\n                            tab::HeadingOptions::TrashedOn\n                        } else {\n                            tab::HeadingOptions::Modified\n                        },\n                        true,\n                    ),\n                    sort_item(\n                        fl!(\"sort-smallest-to-largest\"),\n                        tab::HeadingOptions::Size,\n                        true,\n                    ),\n                    sort_item(\n                        fl!(\"sort-largest-to-smallest\"),\n                        tab::HeadingOptions::Size,\n                        false,\n                    ),\n                    //TODO: sort by type\n                ],\n            ),\n        ),\n        menu::Tree::with_children(\n            Element::from(\n                widget::button::icon(widget::icon::from_name(\"view-more-symbolic\"))\n                    // This prevents the button from being shown as insensitive\n                    .on_press(Message::None)\n                    .padding(8),\n            ),\n            menu::items(\n                key_binds,\n                vec![\n                    menu::Item::Button(fl!(\"zoom-in\"), None, Action::ZoomIn),\n                    menu::Item::Button(fl!(\"default-size\"), None, Action::ZoomDefault),\n                    menu::Item::Button(fl!(\"zoom-out\"), None, Action::ZoomOut),\n                    menu::Item::Divider,\n                    menu::Item::CheckBox(\n                        fl!(\"show-hidden-files\"),\n                        None,\n                        tab.config.show_hidden,\n                        Action::ToggleShowHidden,\n                    ),\n                    menu::Item::CheckBox(\n                        fl!(\"list-directories-first\"),\n                        None,\n                        tab.config.folders_first,\n                        Action::ToggleFoldersFirst,\n                    ),\n                    menu::Item::CheckBox(fl!(\"show-details\"), None, show_details, Action::Preview),\n                    menu::Item::Divider,\n                    menu_button_optional(\n                        fl!(\"gallery-preview\"),\n                        Action::Gallery,\n                        selected_gallery > 0,\n                    ),\n                ],\n            ),\n        ),\n    ])\n    .item_height(ItemHeight::Dynamic(40))\n    .item_width(ItemWidth::Uniform(360))\n    .spacing(theme::spacing().space_xxxs.into())\n    .into()\n}\n\npub fn menu_bar<'a>(\n    core: &Core,\n    tab_opt: Option<&Tab>,\n    config: &Config,\n    modifiers: &Modifiers,\n    key_binds: &HashMap<KeyBind, Action>,\n    clipboard_paste_available: bool,\n) -> Element<'a, Message> {\n    let sort_options = tab_opt.map(Tab::sort_options);\n    let sort_item = |label, sort, dir| {\n        menu::Item::CheckBox(\n            label,\n            None,\n            sort_options.is_some_and(|(sort_name, sort_direction, _)| {\n                sort_name == sort && sort_direction == dir\n            }),\n            Action::SetSort(sort, dir),\n        )\n    };\n    let in_trash = tab_opt.is_some_and(|tab| tab.location.is_trash());\n\n    let mut selected_dir = 0;\n    let mut selected = 0;\n    let mut selected_gallery = 0;\n    if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {\n        for item in items {\n            if item.selected {\n                selected += 1;\n                if item.metadata.is_dir() {\n                    selected_dir += 1;\n                }\n                if item.can_gallery() {\n                    selected_gallery += 1;\n                }\n            }\n        }\n    }\n\n    // Allow paste when clipboard has data and we're in a location that supports it\n    let can_paste =\n        clipboard_paste_available && tab_opt.is_some_and(|tab| tab.location.supports_paste());\n\n    let (delete_item, delete_item_action) = if in_trash || modifiers.shift() {\n        (fl!(\"delete-permanently\"), Action::Delete)\n    } else {\n        (fl!(\"move-to-trash\"), Action::Delete)\n    };\n\n    responsive_menu_bar()\n        .item_height(ItemHeight::Dynamic(40))\n        .item_width(ItemWidth::Uniform(360))\n        .spacing(theme::spacing().space_xxxs.into())\n        .into_element(\n            core,\n            key_binds,\n            MENU_ID.clone(),\n            Message::Surface,\n            vec![\n                (\n                    fl!(\"file\"),\n                    vec![\n                        menu::Item::Button(fl!(\"new-tab\"), None, Action::TabNew),\n                        menu::Item::Button(fl!(\"new-window\"), None, Action::WindowNew),\n                        menu::Item::Button(fl!(\"new-folder\"), None, Action::NewFolder),\n                        menu::Item::Button(fl!(\"new-file\"), None, Action::NewFile),\n                        menu_button_optional(\n                            fl!(\"open\"),\n                            Action::Open,\n                            (selected > 0 && selected_dir == 0)\n                                || (selected_dir == 1 && selected == 1),\n                        ),\n                        menu_button_optional(\n                            fl!(\"menu-open-with\"),\n                            Action::OpenWith,\n                            selected == 1,\n                        ),\n                        menu::Item::Divider,\n                        menu_button_optional(fl!(\"rename\"), Action::Rename, selected > 0),\n                        menu::Item::Divider,\n                        menu::Item::Button(fl!(\"reload-folder\"), None, Action::Reload),\n                        menu::Item::Divider,\n                        menu_button_optional(\n                            fl!(\"add-to-sidebar\"),\n                            Action::AddToSidebar,\n                            selected > 0,\n                        ),\n                        menu::Item::Divider,\n                        menu_button_optional(\n                            fl!(\"restore-from-trash\"),\n                            Action::RestoreFromTrash,\n                            selected > 0 && in_trash,\n                        ),\n                        menu_button_optional(delete_item, delete_item_action, selected > 0),\n                        menu::Item::Divider,\n                        menu::Item::Button(fl!(\"close-tab\"), None, Action::TabClose),\n                        menu::Item::Button(fl!(\"quit\"), None, Action::WindowClose),\n                    ],\n                ),\n                (\n                    (fl!(\"edit\")),\n                    vec![\n                        menu_button_optional(fl!(\"cut\"), Action::Cut, selected > 0),\n                        menu_button_optional(fl!(\"copy\"), Action::Copy, selected > 0),\n                        menu_button_optional(fl!(\"move-to\"), Action::MoveTo, selected > 0),\n                        menu_button_optional(fl!(\"copy-to\"), Action::CopyTo, selected > 0),\n                        menu_button_optional(fl!(\"paste\"), Action::Paste, can_paste),\n                        menu::Item::Button(fl!(\"select-all\"), None, Action::SelectAll),\n                        menu::Item::Divider,\n                        menu::Item::Button(fl!(\"history\"), None, Action::EditHistory),\n                    ],\n                ),\n                (\n                    (fl!(\"view\")),\n                    vec![\n                        menu::Item::Button(fl!(\"zoom-in\"), None, Action::ZoomIn),\n                        menu::Item::Button(fl!(\"default-size\"), None, Action::ZoomDefault),\n                        menu::Item::Button(fl!(\"zoom-out\"), None, Action::ZoomOut),\n                        menu::Item::Divider,\n                        menu::Item::CheckBox(\n                            fl!(\"grid-view\"),\n                            None,\n                            tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::Grid)),\n                            Action::TabViewGrid,\n                        ),\n                        menu::Item::CheckBox(\n                            fl!(\"list-view\"),\n                            None,\n                            tab_opt.is_some_and(|tab| matches!(tab.config.view, tab::View::List)),\n                            Action::TabViewList,\n                        ),\n                        menu::Item::Divider,\n                        menu::Item::CheckBox(\n                            fl!(\"show-hidden-files\"),\n                            None,\n                            tab_opt.is_some_and(|tab| tab.config.show_hidden),\n                            Action::ToggleShowHidden,\n                        ),\n                        menu::Item::CheckBox(\n                            fl!(\"list-directories-first\"),\n                            None,\n                            tab_opt.is_some_and(|tab| tab.config.folders_first),\n                            Action::ToggleFoldersFirst,\n                        ),\n                        menu::Item::CheckBox(\n                            fl!(\"show-details\"),\n                            None,\n                            config.show_details,\n                            Action::Preview,\n                        ),\n                        menu::Item::Divider,\n                        menu_button_optional(\n                            fl!(\"gallery-preview\"),\n                            Action::Gallery,\n                            selected_gallery > 0,\n                        ),\n                        menu::Item::Divider,\n                        menu::Item::Button(fl!(\"menu-settings\"), None, Action::Settings),\n                        menu::Item::Divider,\n                        menu::Item::Button(fl!(\"menu-about\"), None, Action::About),\n                    ],\n                ),\n                (\n                    (fl!(\"sort\")),\n                    vec![\n                        sort_item(fl!(\"sort-a-z\"), tab::HeadingOptions::Name, true),\n                        sort_item(fl!(\"sort-z-a\"), tab::HeadingOptions::Name, false),\n                        sort_item(\n                            fl!(\"sort-newest-first\"),\n                            if in_trash {\n                                tab::HeadingOptions::TrashedOn\n                            } else {\n                                tab::HeadingOptions::Modified\n                            },\n                            false,\n                        ),\n                        sort_item(\n                            fl!(\"sort-oldest-first\"),\n                            if in_trash {\n                                tab::HeadingOptions::TrashedOn\n                            } else {\n                                tab::HeadingOptions::Modified\n                            },\n                            true,\n                        ),\n                        sort_item(\n                            fl!(\"sort-smallest-to-largest\"),\n                            tab::HeadingOptions::Size,\n                            true,\n                        ),\n                        sort_item(\n                            fl!(\"sort-largest-to-smallest\"),\n                            tab::HeadingOptions::Size,\n                            false,\n                        ),\n                        //TODO: sort by type\n                    ],\n                ),\n            ],\n        )\n}\n\npub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {\n    //TODO: only add some of these when in App mode\n    let children = [\n        menu_button!(text::body(fl!(\"open-in-new-tab\")))\n            .on_press(tab::Message::LocationMenuAction(\n                LocationMenuAction::OpenInNewTab(ancestor_index),\n            ))\n            .into(),\n        menu_button!(text::body(fl!(\"open-in-new-window\")))\n            .on_press(tab::Message::LocationMenuAction(\n                LocationMenuAction::OpenInNewWindow(ancestor_index),\n            ))\n            .into(),\n        divider::horizontal::light().into(),\n        menu_button!(text::body(fl!(\"show-details\")))\n            .on_press(tab::Message::LocationMenuAction(\n                LocationMenuAction::Preview(ancestor_index),\n            ))\n            .into(),\n        divider::horizontal::light().into(),\n        menu_button!(text::body(fl!(\"add-to-sidebar\")))\n            .on_press(tab::Message::LocationMenuAction(\n                LocationMenuAction::AddToSidebar(ancestor_index),\n            ))\n            .into(),\n    ];\n\n    container(column::with_children(children))\n        .padding(1)\n        .style(|theme| {\n            let cosmic = theme.cosmic();\n            let component = &cosmic.background.component;\n            container::Style {\n                icon_color: Some(component.on.into()),\n                text_color: Some(component.on.into()),\n                background: Some(Background::Color(component.base.into())),\n                border: Border {\n                    radius: cosmic.radius_s().map(|x| x + 1.0).into(),\n                    width: 1.0,\n                    color: component.divider.into(),\n                },\n                ..Default::default()\n            }\n        })\n        .width(Length::Fixed(360.0))\n        .into()\n}\n"
  },
  {
    "path": "src/mime_app.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse bstr::{BString, ByteSlice, ByteVec};\n#[cfg(feature = \"desktop\")]\nuse cosmic::desktop;\nuse cosmic::widget;\npub use mime_guess::Mime;\nuse rustc_hash::FxHashMap;\nuse std::cmp::Ordering;\nuse std::ffi::OsStr;\nuse std::os::unix::ffi::OsStrExt;\nuse std::path::{Path, PathBuf};\nuse std::time::Instant;\nuse std::{fs, io, process};\n\npub fn exec_to_command(\n    exec: &str,\n    entry_name: &str,\n    entry_path: Option<&Path>,\n    path_opt: &[impl AsRef<OsStr>],\n) -> Option<Vec<process::Command>> {\n    let arguments = shlex::split(exec)?;\n\n    if arguments.is_empty() {\n        tracing::error!(\"command does not contain any arguments\");\n        return None;\n    }\n\n    let mut commands = Vec::new();\n\n    let paths = path_opt\n        .iter()\n        .map(AsRef::as_ref)\n        .map(Some)\n        // Add a single `None` if no path was given.\n        .chain(std::iter::repeat_n(\n            None,\n            if path_opt.is_empty() { 1 } else { 0 },\n        ));\n\n    for path in paths {\n        let mut batch_process = false;\n        let mut args = Vec::with_capacity(arguments.len());\n        let mut field_code_used = false;\n\n        for argument in arguments.iter().skip(1) {\n            let mut new_argument = BString::new(Vec::with_capacity(argument.capacity()));\n            let mut chars = argument.chars();\n            while let Some(char) = chars.next() {\n                // https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html\n                if char == '%' {\n                    match chars.next() {\n                        Some('%') => new_argument.push_char(char),\n                        Some('c') => new_argument.push_str(entry_name),\n                        Some('k') => {\n                            if let Some(path) = entry_path {\n                                new_argument.push_str(path.as_os_str().as_bytes());\n                            }\n                        }\n\n                        // %f and %u behave the same in a file manager.\n                        Some('f' | 'u') => {\n                            if let Some(path) = path\n                                && !field_code_used\n                            {\n                                // TODO: files on remote file systems should be copied to a temporary local file.\n                                batch_process = true;\n                                field_code_used = true;\n                                new_argument.push_str(path.as_bytes());\n                            }\n                        }\n\n                        // %F and %U behave the same in a file manager.\n                        Some('F') | Some('U') => {\n                            if !field_code_used && new_argument.is_empty() {\n                                field_code_used = true;\n                                for path in path_opt.iter().map(AsRef::as_ref) {\n                                    args.push(BString::new(path.as_bytes().to_owned()));\n                                }\n                            }\n                        }\n\n                        _ => (),\n                    }\n                } else {\n                    new_argument.push_char(char);\n                }\n            }\n\n            if !new_argument.is_empty() {\n                args.push(new_argument);\n            }\n        }\n\n        let mut command = process::Command::new(&arguments[0]);\n\n        for arg in args {\n            match arg.to_os_str() {\n                Ok(arg) => {\n                    command.arg(arg);\n                }\n                Err(_) => {\n                    tracing::error!(\"invalid string encoding in command\");\n                    return None;\n                }\n            }\n        }\n\n        commands.push(command);\n\n        if !batch_process {\n            break;\n        }\n    }\n\n    #[cfg(debug_assertions)]\n    for command in &commands {\n        log::debug!(\n            \"Parsed program {} with args: {:?}\",\n            command.get_program().to_string_lossy(),\n            command.get_args()\n        );\n    }\n\n    Some(commands)\n}\n\n#[derive(Clone, Debug)]\npub struct MimeApp {\n    pub id: String,\n    pub path: Option<PathBuf>,\n    pub name: String,\n    pub exec: Option<String>,\n    pub icon: widget::icon::Handle,\n    pub is_default: bool,\n}\n\nimpl MimeApp {\n    //TODO: move to libcosmic, support multiple files\n    pub fn command<O: AsRef<OsStr>>(&self, path_opt: &[O]) -> Option<Vec<process::Command>> {\n        exec_to_command(\n            self.exec.as_deref()?,\n            &self.name,\n            self.path.as_deref(),\n            path_opt,\n        )\n    }\n}\n\n// This allows usage of MimeApp in a dropdown\nimpl AsRef<str> for MimeApp {\n    fn as_ref(&self) -> &str {\n        &self.name\n    }\n}\n\n#[cfg(feature = \"desktop\")]\nimpl From<&desktop::DesktopEntryData> for MimeApp {\n    fn from(app: &desktop::DesktopEntryData) -> Self {\n        Self {\n            id: app.id.clone(),\n            path: app.path.clone(),\n            name: app.name.clone(),\n            exec: app.exec.clone(),\n            icon: match &app.icon {\n                desktop::fde::IconSource::Name(name) => {\n                    widget::icon::from_name(name.as_str()).size(32).handle()\n                }\n                desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()),\n            },\n            is_default: false,\n        }\n    }\n}\n\n#[cfg(feature = \"desktop\")]\nfn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {\n    path_opt\n        .as_ref()\n        .and_then(|path| path.file_name())\n        .is_some_and(|x| x == filename)\n}\n\npub struct MimeAppCache {\n    apps: Vec<MimeApp>,\n    cache: FxHashMap<Mime, Vec<MimeApp>>,\n    icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,\n    terminals: Vec<MimeApp>,\n}\n\nimpl MimeAppCache {\n    pub fn new() -> Self {\n        let mut mime_app_cache = Self {\n            apps: Vec::new(),\n            cache: FxHashMap::default(),\n            icons: FxHashMap::default(),\n            terminals: Vec::new(),\n        };\n        mime_app_cache.reload();\n        mime_app_cache\n    }\n\n    #[cfg(not(feature = \"desktop\"))]\n    pub fn reload(&mut self) {}\n\n    // Only available when using desktop feature of libcosmic, which only works on Unix-likes\n    #[cfg(feature = \"desktop\")]\n    pub fn reload(&mut self) {\n        use crate::localize::LANGUAGE_SORTER;\n\n        let start = Instant::now();\n\n        self.apps.clear();\n        self.cache.clear();\n        self.icons.clear();\n        self.terminals.clear();\n\n        //TODO: get proper locale?\n        let locale = &[];\n\n        // Load desktop applications by supported mime types\n        //TODO: hashmap for all apps by id?\n        let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();\n        for app in &all_apps {\n            //TODO: just collect apps that can be executed with a file argument?\n            if !app.mime_types.is_empty() {\n                self.apps.push(MimeApp::from(app));\n            }\n            for mime in &app.mime_types {\n                let apps = self\n                    .cache\n                    .entry(mime.clone())\n                    .or_insert_with(|| Vec::with_capacity(1));\n                if !apps.iter().any(|x| x.id == app.id) {\n                    apps.push(MimeApp::from(app));\n                }\n            }\n            for category in &app.categories {\n                if category == \"TerminalEmulator\" {\n                    self.terminals.push(MimeApp::from(app));\n                    break;\n                }\n            }\n        }\n\n        let mut list = cosmic_mime_apps::List::default();\n\n        let paths = cosmic_mime_apps::list_paths();\n        list.load_from_paths(&paths);\n\n        for (mime, filenames) in list\n            .added_associations\n            .iter()\n            .chain(list.default_apps.iter())\n        {\n            for filename in filenames {\n                log::trace!(\"add {mime}={filename}\");\n                let apps = self\n                    .cache\n                    .entry(mime.clone())\n                    .or_insert_with(|| Vec::with_capacity(1));\n                if !apps.iter().any(|x| filename_eq(&x.path, filename)) {\n                    if let Some(app) = all_apps.iter().find(|&x| filename_eq(&x.path, filename)) {\n                        apps.push(MimeApp::from(app));\n                    } else {\n                        log::info!(\n                            \"failed to add association for {mime:?}: application {filename:?} not found\"\n                        );\n                    }\n                }\n            }\n        }\n\n        for (mime, filenames) in list.removed_associations.iter() {\n            for filename in filenames {\n                log::trace!(\"remove {mime}={filename}\");\n                if let Some(apps) = self.cache.get_mut(mime) {\n                    apps.retain(|x| !filename_eq(&x.path, filename));\n                }\n            }\n        }\n\n        for (mime, filenames) in list.default_apps.iter() {\n            for filename in filenames {\n                log::trace!(\"default {mime}={filename}\");\n                if let Some(apps) = self.cache.get_mut(mime) {\n                    let mut found = false;\n                    for app in apps.iter_mut() {\n                        if filename_eq(&app.path, filename) {\n                            app.is_default = true;\n                            found = true;\n                        } else {\n                            app.is_default = false;\n                        }\n                    }\n                    if found {\n                        break;\n                    }\n                    log::debug!(\n                        \"failed to set default for {mime:?}: application {filename:?} not found\"\n                    );\n                }\n            }\n        }\n\n        // Sort apps by name\n        self.apps\n            .sort_by(|a, b| match (a.is_default, b.is_default) {\n                (true, false) => Ordering::Less,\n                (false, true) => Ordering::Greater,\n                _ => LANGUAGE_SORTER.compare(&a.name, &b.name),\n            });\n        for apps in self.cache.values_mut() {\n            apps.sort_by(|a, b| match (a.is_default, b.is_default) {\n                (true, false) => Ordering::Less,\n                (false, true) => Ordering::Greater,\n                _ => LANGUAGE_SORTER.compare(&a.name, &b.name),\n            });\n        }\n\n        // Copy icons to special cache\n        //TODO: adjust dropdown API so this is no longer needed\n        self.icons.extend(self.cache.iter().map(|(mime, apps)| {\n            (\n                mime.clone(),\n                apps.iter().map(|app| app.icon.clone()).collect(),\n            )\n        }));\n\n        let elapsed = start.elapsed();\n        log::info!(\"loaded mime app cache in {elapsed:?}\");\n    }\n\n    pub fn apps(&self) -> &[MimeApp] {\n        &self.apps\n    }\n\n    pub fn get(&self, key: &Mime) -> &[MimeApp] {\n        self.cache.get(key).map_or(&[], Vec::as_slice)\n    }\n\n    pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {\n        self.icons.get(key).map_or(&[], Box::as_ref)\n    }\n\n    fn get_default_terminal(&self) -> Option<String> {\n        let output = process::Command::new(\"xdg-mime\")\n            .args([\"query\", \"default\", \"x-scheme-handler/terminal\"])\n            .output()\n            .ok()?;\n\n        if !output.status.success() {\n            return None;\n        }\n\n        String::from_utf8(output.stdout)\n            .ok()\n            .map(|string| string.trim().replace(\".desktop\", \"\"))\n    }\n\n    pub fn terminal(&self) -> Option<&MimeApp> {\n        //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec\n        // The current approach works but might not adhere to the spec (yet)\n\n        // Look for and return preferred terminals\n        //TODO: fallback order beyond cosmic-term?\n\n        let mut preference_order = vec![\"com.system76.CosmicTerm\".to_string()];\n\n        if let Some(id) = self.get_default_terminal() {\n            preference_order.insert(0, id);\n        }\n\n        for id in &preference_order {\n            for terminal in &self.terminals {\n                if &terminal.id == id {\n                    return Some(terminal);\n                }\n            }\n        }\n\n        // Return whatever was the first terminal found\n        self.terminals.first()\n    }\n\n    #[cfg(not(feature = \"desktop\"))]\n    pub fn set_default(&mut self, mime: Mime, id: String) {\n        log::warn!(\n            \"failed to set default handler for {mime:?} to {id:?}: desktop feature not enabled\"\n        );\n    }\n\n    #[cfg(feature = \"desktop\")]\n    pub fn set_default(&mut self, mime: Mime, mut id: String) {\n        let Some(path) = cosmic_mime_apps::local_list_path() else {\n            log::warn!(\"failed to find mimeapps.list path\");\n            return;\n        };\n\n        let mut list = cosmic_mime_apps::List::default();\n        match fs::read_to_string(&path) {\n            Ok(string) => {\n                list.load_from(&string);\n            }\n            Err(err) => {\n                if err.kind() != io::ErrorKind::NotFound {\n                    log::warn!(\"failed to read {}: {}\", path.display(), err);\n                    return;\n                }\n            }\n        }\n\n        let suffix = \".desktop\";\n        if !id.ends_with(suffix) {\n            id.push_str(suffix);\n        }\n        list.set_default_app(mime, id);\n\n        let mut string = list.to_string();\n        string.push('\\n');\n        match fs::write(&path, string) {\n            Ok(()) => {\n                self.reload();\n            }\n            Err(err) => {\n                log::warn!(\"failed to write {}: {}\", path.display(), err);\n            }\n        }\n    }\n}\n\nimpl Default for MimeAppCache {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::exec_to_command;\n\n    #[test]\n    fn keys_within_words() {\n        let exec = \"/usr/bin/foo --option=%f\";\n        let paths = [\"file1\"];\n        let commands = exec_to_command(exec, \"keys_within_words\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\"/usr/bin/foo\", command.get_program().to_str().unwrap());\n        assert_eq!(\n            \"--option=file1\",\n            command.get_args().next().unwrap().to_str().unwrap()\n        );\n    }\n\n    #[test]\n    fn no_path_f_field_code() {\n        let exec = \"/usr/bin/foo %f\";\n        let paths: [&str; 0] = [];\n        let commands = exec_to_command(exec, \"no_path_f_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\"/usr/bin/foo\", command.get_program().to_str().unwrap());\n        assert_eq!(0, command.get_args().len());\n    }\n\n    #[test]\n    fn one_path_f_field_code() {\n        let exec = \"/usr/bin/foo %f\";\n        let paths = [\"file1\"];\n        let commands = exec_to_command(exec, \"one_path_f_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\"/usr/bin/foo\", command.get_program().to_str().unwrap());\n        assert_eq!(\n            \"file1\",\n            command.get_args().next().unwrap().to_str().unwrap()\n        );\n    }\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn one_path_F_field_code() {\n        let exec = \"/usr/bin/cosmic-term -w %F\";\n        let paths = [\"/home/user\"];\n        let commands = exec_to_command(exec, \"one_path_F_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n        let mut args = command.get_args();\n\n        assert_eq!(\n            \"/usr/bin/cosmic-term\",\n            command.get_program().to_str().unwrap()\n        );\n        assert_eq!(\"-w\", args.next().unwrap().to_str().unwrap());\n        assert_eq!(paths[0], args.next().unwrap().to_str().unwrap());\n    }\n\n    #[test]\n    fn one_path_u_field_code() {\n        let exec = \"/usr/bin/cosmic-term -w %u\";\n        let paths = [\"/home/user\"];\n        let commands = exec_to_command(exec, \"one_path_u_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n        let mut args = command.get_args();\n\n        assert_eq!(\n            \"/usr/bin/cosmic-term\",\n            command.get_program().to_str().unwrap()\n        );\n        assert_eq!(\"-w\", args.next().unwrap().to_str().unwrap());\n        assert_eq!(paths[0], args.next().unwrap().to_str().unwrap());\n    }\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn one_path_U_field_code() {\n        let exec = \"/usr/bin/rmrfbye %U\";\n        let paths = [\"/\"];\n        let commands = exec_to_command(exec, \"one_path_U_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\"/usr/bin/rmrfbye\", command.get_program().to_str().unwrap());\n        assert_eq!(\"/\", command.get_args().next().unwrap().to_str().unwrap());\n    }\n\n    #[test]\n    fn mult_path_f_field_code() {\n        let exec = \"/usr/games/ppsspp %f\";\n        let paths = [\n            \"/usr/share/games/psp/miku.iso\",\n            \"/usr/share/games/psp/eternia.iso\",\n        ];\n        let commands = exec_to_command(exec, \"mult_path_f_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(paths.len(), commands.len());\n        for (command, path) in commands.into_iter().zip(paths.iter()) {\n            assert_eq!(\"/usr/games/ppsspp\", command.get_program().to_str().unwrap());\n\n            assert_eq!(1, command.get_args().len());\n            let command_path = command.get_args().next().unwrap();\n            assert_eq!(*path, command_path.to_str().unwrap());\n        }\n    }\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn mult_path_F_field_code() {\n        let exec = \"/usr/games/gzdoom %F\";\n        let paths = [\n            \"/usr/share/games/doom2/hr.wad\",\n            \"/usr/share/games/doom2/hrmus.wad\",\n        ];\n        let commands = exec_to_command(exec, \"mult_path_F_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\"/usr/games/gzdoom\", command.get_program().to_str().unwrap());\n        assert!(\n            paths\n                .iter()\n                .zip(command.get_args())\n                .all(|(&expected, actual)| expected == actual.to_string_lossy())\n        );\n    }\n\n    #[test]\n    fn mult_path_u_field_code() {\n        let exec = \"/usr/bin/cosmic_browser %u\";\n        let paths = [\n            \"file:///home/josh/Books/osstep.pdf\",\n            \"https://redox-os.org/\",\n            \"https://system76.com/\",\n        ];\n        let commands = exec_to_command(exec, \"mult_path_u_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(paths.len(), commands.len());\n        for (command, path) in commands.into_iter().zip(paths.iter()) {\n            assert_eq!(\n                \"/usr/bin/cosmic_browser\",\n                command.get_program().to_str().unwrap()\n            );\n\n            assert_eq!(1, command.get_args().len());\n            let command_path = command.get_args().next().unwrap();\n            assert_eq!(*path, command_path.to_str().unwrap());\n        }\n    }\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn mult_path_U_field_code() {\n        let exec = \"/usr/bin/mpv %U\";\n        let paths = [\n            \"frieren01.mkv\",\n            \"rtmp://example.org/this/video/doesnt/exist.avi\",\n        ];\n        let commands = exec_to_command(exec, \"mult_path_U_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n        assert_eq!(paths.len(), command.get_args().count());\n\n        assert_eq!(\"/usr/bin/mpv\", command.get_program().to_str().unwrap());\n        assert!(\n            paths\n                .iter()\n                .zip(command.get_args())\n                .all(|(&expected, actual)| expected == actual.to_string_lossy())\n        );\n    }\n\n    #[test]\n    fn flatpak_style_exec() {\n        // Tests args before field codes\n        let exec = \"/usr/bin/flatpak run --branch=stable --command=ferris --file-forwarding org.joshfake.ferris @@u %U\";\n        let args = [\n            \"run\",\n            \"--branch=stable\",\n            \"--command=ferris\",\n            \"--file-forwarding\",\n            \"org.joshfake.ferris\",\n            \"@@u\",\n        ];\n        let paths = [\"file1.rs\", \"file2.rs\"];\n        let commands = exec_to_command(exec, \"flatpak_style_exec\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n        assert_eq!(args.len() + paths.len(), command.get_args().count());\n\n        assert_eq!(\"/usr/bin/flatpak\", command.get_program().to_str().unwrap());\n        assert!(\n            args.iter()\n                .chain(paths.iter())\n                .zip(command.get_args())\n                .all(|(&expected, actual)| expected == actual.to_string_lossy())\n        );\n    }\n\n    #[test]\n    fn multiple_field_codes() {\n        // Tests that only one field code is used rather than passing paths to each field code\n        let exec = \"/usr/games/roguelike %U %f\";\n        let paths = [\n            \"file:///usr/share/games/roguelike/mods/mod1\",\n            \"file:///usr/share/games/roguelike/mods/mod2\",\n        ];\n        let commands = exec_to_command(exec, \"multiple_field_codes\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n\n        assert_eq!(\n            \"/usr/games/roguelike\",\n            command.get_program().to_str().unwrap()\n        );\n        assert!(\n            paths\n                .iter()\n                .zip(command.get_args())\n                .all(|(&expected, actual)| expected == actual.to_string_lossy())\n        );\n    }\n\n    #[test]\n    fn sandwiched_field_code() {\n        // Tests that arguments before and after the field code works\n        // (Borrowed from KDE because someone had this exact line in an issue)\n        let exec = \"/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=okular --file-forwarding org.kde.okular @@u %U @@\";\n        let args_leading = [\n            \"run\",\n            \"--branch=stable\",\n            \"--arch=x86_64\",\n            \"--command=okular\",\n            \"--file-forwarding\",\n            \"org.kde.okular\",\n            \"@@u\",\n        ];\n        let paths = [\"rust_game_dev.pdf\", \"superhero_ferris.epub\"];\n        let args_trailing = [\"@@\"];\n        let commands = exec_to_command(exec, \"sandwiched_field_code\", None, &paths)\n            .expect(\"Should parse valid exec\");\n\n        assert_eq!(1, commands.len());\n        let command = commands.first().unwrap();\n        assert_eq!(\n            args_leading.len() + paths.len() + args_trailing.len(),\n            command.get_args().len()\n        );\n\n        assert_eq!(\"/usr/bin/flatpak\", command.get_program().to_str().unwrap());\n        assert!(\n            args_leading\n                .iter()\n                .chain(paths.iter())\n                .chain(args_trailing.iter())\n                .zip(command.get_args())\n                .all(|(&expected, actual)| expected == actual.to_string_lossy())\n        );\n    }\n}\n"
  },
  {
    "path": "src/mime_icon.rs",
    "content": "// SPDX-License-Identifier: GPL-3.0-only\n\nuse cosmic::widget::icon;\nuse mime_guess::Mime;\nuse rustc_hash::FxHashMap;\nuse std::fs;\nuse std::path::Path;\nuse std::sync::{LazyLock, Mutex};\n\npub const FALLBACK_MIME_ICON: &str = \"text-x-generic\";\n\n#[derive(Debug, Eq, Hash, PartialEq)]\nstruct MimeIconKey {\n    mime: Mime,\n    size: u16,\n}\n\nstruct MimeIconCache {\n    cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,\n    #[cfg(unix)]\n    shared_mime_info: xdg_mime::SharedMimeInfo,\n}\n\nimpl MimeIconCache {\n    pub fn new() -> Self {\n        Self {\n            cache: FxHashMap::default(),\n            #[cfg(unix)]\n            shared_mime_info: xdg_mime::SharedMimeInfo::new(),\n        }\n    }\n\n    #[cfg(not(unix))]\n    pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {\n        None\n    }\n\n    #[cfg(unix)]\n    pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {\n        self.cache\n            .entry(key)\n            .or_insert_with_key(|key| {\n                let mut icon_names = self.shared_mime_info.lookup_icon_names(&key.mime);\n                if icon_names.is_empty() {\n                    return None;\n                }\n                let icon_name = icon_names.remove(0);\n                let mut named = icon::from_name(icon_name).size(key.size);\n                if !icon_names.is_empty() {\n                    let fallback_names =\n                        icon_names.into_iter().map(std::borrow::Cow::from).collect();\n                    named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));\n                }\n                Some(named.handle())\n            })\n            .clone()\n    }\n}\nstatic MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =\n    LazyLock::new(|| Mutex::new(MimeIconCache::new()));\n\n#[cfg(not(unix))]\npub fn mime_for_path(\n    path: impl AsRef<Path>,\n    metadata_opt: Option<&fs::Metadata>,\n    remote: bool,\n) -> Mime {\n    mime_guess::from_path(path).first_or_octet_stream()\n}\n\n#[cfg(unix)]\npub fn mime_for_path(\n    path: impl AsRef<Path>,\n    metadata_opt: Option<&fs::Metadata>,\n    remote: bool,\n) -> Mime {\n    let path = path.as_ref();\n    let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();\n    // Try the shared mime info cache first\n    let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();\n    if remote {\n        if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {\n            gb.file_name(file_name);\n        }\n    } else {\n        gb.path(path);\n    }\n    if let Some(metadata) = metadata_opt {\n        gb.metadata(metadata.clone());\n    }\n    let guess = gb.guess();\n    let guessed_mime = guess.mime_type();\n\n    /// Checks if the `Mime` is a special variant returned by `xdg-mime`.\n    /// This includes directories, symlinks and zerosize files, which are returned as uncertain.\n    fn is_special_mime(mime: &Mime) -> bool {\n        *mime == \"inode/directory\" || *mime == \"inode/symlink\" || *mime == \"application/x-zerosize\"\n    }\n\n    // `xdg-mime-rs` sets the guess to uncertain if it returns special mime types.\n    // The guess could also be uncertain on platforms without shared-mime-info.\n    // Try mime_guess, but only if it is not one of the special mime types.\n    if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) {\n        // If uncertain, try mime_guess. This could happen on platforms without shared-mime-info\n        mime_guess::from_path(path).first_or_octet_stream()\n    } else {\n        guessed_mime.clone()\n    }\n}\n\npub fn mime_icon(mime: Mime, size: u16) -> icon::Handle {\n    let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();\n    match mime_icon_cache.get(MimeIconKey { mime, size }) {\n        Some(handle) => handle,\n        None => icon::from_name(FALLBACK_MIME_ICON).size(size).handle(),\n    }\n}\n\n#[cfg(not(unix))]\npub fn parent_mime_types(_mime: &Mime) -> Option<Vec<Mime>> {\n    None\n}\n\n#[cfg(unix)]\npub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {\n    let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();\n    mime_icon_cache.shared_mime_info.get_parents_aliased(mime)\n}\n"
  },
  {
    "path": "src/mounter/gvfs.rs",
    "content": "use cosmic::iced::futures::SinkExt;\nuse cosmic::iced::{Subscription, stream};\nuse cosmic::{Task, widget};\nuse gio::glib;\nuse gio::prelude::*;\nuse std::any::TypeId;\nuse std::cell::Cell;\nuse std::future::pending;\nuse std::hash::Hash;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse tokio::sync::mpsc;\n\nuse super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};\nuse crate::config::IconSizes;\nuse crate::err_str;\nuse crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location};\n\nconst TARGET_URI_ATTRIBUTE: &str = \"standard::target-uri\";\n\nfn resolve_uri(uri: &str) -> (String, gio::File) {\n    let file = gio::File::for_uri(uri);\n    // Resolve the target-uri if it exists\n    if let Ok(file_info) = file.query_info(\n        TARGET_URI_ATTRIBUTE,\n        gio::FileQueryInfoFlags::NONE,\n        gio::Cancellable::NONE,\n    ) && let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE)\n    {\n        let resolved_uri = String::from(resolved_uri);\n        let file = gio::File::for_uri(&resolved_uri);\n        return (resolved_uri, file);\n    }\n\n    (uri.to_string(), file)\n}\n\nfn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {\n    if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {\n        for name in themed_icon.names() {\n            let named = widget::icon::from_name(name.as_str()).size(size);\n            if let Some(path) = named.path() {\n                return Some(path);\n            }\n        }\n    }\n    //TODO: handle more gio icon types\n    None\n}\n\nfn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {\n    let mut items: MounterItems = (monitor.mounts().into_iter())\n        .enumerate()\n        // Hide shadowed mounts\n        .filter(|(_, mount)| !mount.is_shadowed())\n        .map(|(i, mount)| {\n            let root = MountExt::root(&mount);\n            let is_remote = root\n                .query_filesystem_info(\n                    gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,\n                    gio::Cancellable::NONE,\n                )\n                .ok()\n                .map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))\n                .unwrap_or(true); // Default to remote if query fails\n\n            MounterItem::Gvfs(Item {\n                uri: mount.root().uri().into(),\n                kind: ItemKind::Mount,\n                index: i,\n                name: mount.name().into(),\n                is_mounted: true,\n                is_remote,\n                icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),\n                icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),\n                path_opt: root.path(),\n            })\n        })\n        .collect();\n    items.extend(\n        (monitor.volumes().into_iter())\n            .enumerate()\n            // Volumes with mounts are already listed by mount\n            .filter(|(_, volume)| volume.get_mount().is_none())\n            .map(|(i, volume)| {\n                let uri = VolumeExt::activation_root(&volume)\n                    .map(|f| f.uri().into())\n                    .unwrap_or_default();\n                MounterItem::Gvfs(Item {\n                    // TODO can we get URI for volumes with no mount?\n                    uri,\n                    kind: ItemKind::Volume,\n                    index: i,\n                    name: volume.name().into(),\n                    is_mounted: false,\n                    is_remote: false,\n                    icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),\n                    icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),\n                    path_opt: None,\n                })\n            }),\n    );\n    items\n}\n\nfn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {\n    let force_dir = uri.starts_with(\"network:///\");\n    let (_, file) = resolve_uri(uri);\n\n    // Read .hidden file if present\n    let hidden_files: Box<[String]> = if let Some(path) = file.path() {\n        let hidden_file_path = path.join(\".hidden\");\n        if hidden_file_path.is_file() {\n            tab::parse_hidden_file(&hidden_file_path)\n        } else {\n            Box::from([])\n        }\n    } else {\n        Box::from([])\n    };\n\n    let mut items = Vec::new();\n    for info_res in file\n        .enumerate_children(\"*\", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)\n        .map_err(err_str)?\n    {\n        let info = info_res.map_err(err_str)?;\n        let name = info.name().to_string_lossy().into_owned();\n        let display_name = String::from(info.display_name());\n\n        let uri = String::from(file.child(info.name()).uri());\n\n        //TODO: what is the best way to resolve shortcuts?\n        let location = Location::Network(uri, display_name.clone(), file.child(&name).path());\n\n        let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {\n            let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);\n            let is_dir = matches!(info.file_type(), gio::FileType::Directory);\n            let size_opt = (!is_dir).then_some(info.size() as u64);\n            let mut children_opt = None;\n\n            if is_dir {\n                if let Some(path) = file.child(&name).path() {\n                    //TODO: calculate children in the background (and make it cancellable?)\n                    match std::fs::read_dir(&path) {\n                        Ok(entries) => {\n                            children_opt = Some(entries.count());\n                        }\n                        Err(err) => {\n                            log::warn!(\"failed to read directory {}: {}\", path.display(), err);\n                            children_opt = Some(0);\n                        }\n                    }\n                } else {\n                    children_opt = Some(0);\n                }\n            }\n            ItemMetadata::GvfsPath {\n                mtime,\n                size_opt,\n                children_opt,\n            }\n        } else {\n            ItemMetadata::SimpleDir { entries: 0 }\n        };\n\n        let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {\n            let file_icon = |size| {\n                info.icon()\n                    .as_ref()\n                    .and_then(|icon| gio_icon_to_path(icon, size))\n                    .map(widget::icon::from_path)\n                    .unwrap_or(\n                        widget::icon::from_name(if metadata.is_dir() {\n                            \"folder\"\n                        } else {\n                            \"text-x-generic\"\n                        })\n                        .size(size)\n                        .handle(),\n                    )\n            };\n            (\n                //TODO: get mime from content_type?\n                \"inode/directory\".parse().unwrap(),\n                file_icon(sizes.grid()),\n                file_icon(sizes.list()),\n                file_icon(sizes.list_condensed()),\n            )\n        };\n\n        // Check if item is hidden\n        let hidden = name.starts_with('.')\n            || info.boolean(gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)\n            || hidden_files.contains(&name);\n\n        items.push(tab::Item {\n            name,\n            is_mount_point: false,\n            display_name,\n            metadata,\n            hidden,\n            location_opt: Some(location),\n            image_dimensions: None,\n            mime,\n            icon_handle_grid,\n            icon_handle_list,\n            icon_handle_list_condensed,\n            thumbnail_opt: Some(ItemThumbnail::NotImage),\n            button_id: widget::Id::unique(),\n            pos_opt: Cell::new(None),\n            rect_opt: Cell::new(None),\n            selected: false,\n            highlighted: false,\n            overlaps_drag_rect: false,\n            //TODO: scan directory size on gvfs mounts?\n            dir_size: DirSize::NotDirectory,\n            cut: false,\n        });\n    }\n    Ok(items)\n}\n\nfn dir_info(uri: &str) -> Result<(String, String, Option<PathBuf>), glib::Error> {\n    let (resolved_uri, file) = resolve_uri(uri);\n    let info = file.query_info(\n        gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,\n        gio::FileQueryInfoFlags::NONE,\n        gio::Cancellable::NONE,\n    )?;\n\n    Ok((resolved_uri, info.display_name().into(), file.path()))\n}\n\nfn mount_op(\n    uri: String,\n    event_tx: std::sync::Weak<crate::channel::Sender<Event>>,\n) -> gio::MountOperation {\n    let mount_op = gio::MountOperation::new();\n    mount_op.connect_ask_password(\n        move |mount_op, message, default_user, default_domain, flags| {\n            let auth = MounterAuth {\n                message: message.to_string(),\n                username_opt: flags\n                    .contains(gio::AskPasswordFlags::NEED_USERNAME)\n                    .then(|| default_user.to_string()),\n                domain_opt: flags\n                    .contains(gio::AskPasswordFlags::NEED_DOMAIN)\n                    .then(|| default_domain.to_string()),\n                password_opt: flags\n                    .contains(gio::AskPasswordFlags::NEED_PASSWORD)\n                    .then(String::new),\n                remember_opt: flags\n                    .contains(gio::AskPasswordFlags::SAVING_SUPPORTED)\n                    .then_some(false),\n                anonymous_opt: flags\n                    .contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED)\n                    .then_some(false),\n            };\n            let (auth_tx, mut auth_rx) = mpsc::channel(1);\n            if let Some(event_tx) = event_tx.upgrade() {\n                event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx));\n            }\n            //TODO: async recv?\n            if let Some(auth) = auth_rx.blocking_recv() {\n                if auth.anonymous_opt == Some(true) {\n                    mount_op.set_anonymous(true);\n                } else {\n                    mount_op.set_username(auth.username_opt.as_deref());\n                    mount_op.set_domain(auth.domain_opt.as_deref());\n                    mount_op.set_password(auth.password_opt.as_deref());\n                    if auth.remember_opt == Some(true) {\n                        mount_op.set_password_save(gio::PasswordSave::Permanently);\n                    }\n                }\n                mount_op.reply(gio::MountOperationResult::Handled);\n            } else {\n                mount_op.reply(gio::MountOperationResult::Aborted);\n            }\n        },\n    );\n    mount_op\n}\n\nenum Cmd {\n    Items(IconSizes, mpsc::Sender<MounterItems>),\n    Rescan,\n    Mount(\n        MounterItem,\n        tokio::sync::oneshot::Sender<anyhow::Result<()>>,\n    ),\n    NetworkDrive(String, tokio::sync::oneshot::Sender<anyhow::Result<()>>),\n    NetworkScan(\n        String,\n        IconSizes,\n        mpsc::Sender<Result<Vec<tab::Item>, String>>,\n    ),\n    DirInfo(\n        String,\n        mpsc::Sender<Result<(String, String, Option<PathBuf>), glib::Error>>,\n    ),\n    Unmount(MounterItem),\n}\n\nenum Event {\n    Changed,\n    Items(MounterItems),\n    MountResult(MounterItem, Result<bool, String>),\n    NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),\n    NetworkResult(String, Result<bool, String>),\n}\n\n#[derive(Clone, Debug)]\nenum ItemKind {\n    Mount,\n    Volume,\n}\n\n//TODO: better method of matching items\n#[derive(Clone, Debug)]\npub struct Item {\n    uri: String,\n    kind: ItemKind,\n    index: usize,\n    name: String,\n    is_mounted: bool,\n    is_remote: bool,\n    icon_opt: Option<PathBuf>,\n    icon_symbolic_opt: Option<PathBuf>,\n    path_opt: Option<PathBuf>,\n}\n\nimpl Item {\n    pub fn name(&self) -> String {\n        self.name.clone()\n    }\n\n    pub const fn is_mounted(&self) -> bool {\n        self.is_mounted\n    }\n\n    pub const fn is_remote(&self) -> bool {\n        self.is_remote\n    }\n\n    pub fn uri(&self) -> String {\n        self.uri.clone()\n    }\n\n    pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {\n        if symbolic {\n            self.icon_symbolic_opt.as_ref()\n        } else {\n            self.icon_opt.as_ref()\n        }\n        .map(|icon| widget::icon::from_path(icon.clone()))\n    }\n\n    pub fn path(&self) -> Option<PathBuf> {\n        self.path_opt.clone()\n    }\n}\n\npub struct Gvfs {\n    command_tx: mpsc::UnboundedSender<Cmd>,\n    event_rx: Arc<crate::channel::Receiver<Event>>,\n}\n\nimpl Gvfs {\n    pub fn new() -> Self {\n        //TODO: switch to using gvfs-zbus which will better integrate with async rust\n        let (command_tx, mut command_rx) = mpsc::unbounded_channel();\n        let (event_tx, event_rx) = crate::channel::channel();\n        let event_tx = Arc::new(event_tx);\n        std::thread::spawn(move || {\n            let main_loop = glib::MainLoop::new(None, false);\n            main_loop.context().spawn_local(async move {\n                let event_tx = Arc::downgrade(&event_tx);\n                let monitor = gio::VolumeMonitor::get();\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_mount_changed(move |_monitor, mount| {\n                        log::info!(\"mount changed {}\", MountExt::name(mount));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_mount_added(move |_monitor, mount| {\n                        log::info!(\"mount added {}\", MountExt::name(mount));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_mount_removed(move |_monitor, mount| {\n                        log::info!(\"mount removed {}\", MountExt::name(mount));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_volume_changed(move |_monitor, volume| {\n                        log::info!(\"volume changed {}\", VolumeExt::name(volume));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_volume_added(move |_monitor, volume| {\n                        log::info!(\"volume added {}\", VolumeExt::name(volume));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n                {\n                    let event_tx = event_tx.clone();\n                    monitor.connect_volume_removed(move |_monitor, volume| {\n                        log::info!(\"volume removed {}\", VolumeExt::name(volume));\n                        if let Some(event_tx) = event_tx.upgrade() {\n                            event_tx.send(Event::Changed);\n                        }\n                    });\n                }\n\n                while let Some(command) = command_rx.recv().await {\n                    match command {\n                        Cmd::Items(sizes, items_tx) => {\n                            items_tx.send(items(&monitor, sizes)).await.unwrap();\n                        }\n                        Cmd::Rescan => {\n                            let Some(event_tx) = event_tx.upgrade() else {\n                                return;\n                            };\n\n                            event_tx.send(Event::Items(items(&monitor, IconSizes::default())));\n                        }\n                        Cmd::Mount(mounter_item, complete_tx) => {\n                            let MounterItem::Gvfs(ref item) = mounter_item else {\n                                _ = complete_tx.send(Err(anyhow::anyhow!(\"No mounter item\")));\n                                continue\n                            };\n                            let ItemKind::Volume = item.kind else {\n                                _ = complete_tx.send(Err(anyhow::anyhow!(\"No mounter volume\")));\n                                continue\n                            };\n                            for (i, volume) in monitor.volumes().into_iter().enumerate() {\n                                if i != item.index {\n                                    continue;\n                                }\n\n                                let name = VolumeExt::name(&volume);\n                                if item.name != name {\n                                    log::warn!(\"trying to mount volume {} failed: name is {:?} when {:?} was expected\", i, name, item.name);\n                                    continue;\n                                }\n\n                                log::info!(\"mount {name}\");\n                                //TODO: do not use name as a URI for mount_op\n                                let mount_op = mount_op(name.to_string(), event_tx.clone());\n                                let event_tx = event_tx.clone();\n                                let mounter_item = mounter_item.clone();\n                                let volume_for_callback = volume.clone();\n                                VolumeExt::mount(\n                                    &volume,\n                                    gio::MountMountFlags::NONE,\n                                    Some(&mount_op),\n                                    gio::Cancellable::NONE,\n                                    move |res| {\n                                        log::info!(\"mount {name}: result {res:?}\");\n                                        // Update the mounter_item with mount information after successful mount\n                                        let mut updated_item = mounter_item.clone();\n                                        if res.is_ok()\n                                            && let MounterItem::Gvfs(ref mut item) = updated_item\n                                                && let Some(mount) = volume_for_callback.get_mount() {\n                                                    let root = MountExt::root(&mount);\n                                                    item.path_opt = root.path();\n                                                    item.is_mounted = true;\n                                                    // Query if remote\n                                                    item.is_remote = root\n                                                        .query_filesystem_info(\n                                                            gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,\n                                                            gio::Cancellable::NONE,\n                                                        )\n                                                        .ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))\n                                                        .unwrap_or(true);\n                                                }\n                                        let Some(event_tx) = event_tx.upgrade() else {\n                                            return;\n                                        };\n                                        event_tx.send(Event::MountResult(updated_item, match res {\n                                            Ok(()) => {\n                                                _ = complete_tx.send(Ok(()));\n                                                Ok(true)\n                                            },\n                                            Err(err) => {\n                                                _ = complete_tx.send(Err(anyhow::anyhow!(\"{err:?}\")));\n                                                match err.kind::<gio::IOErrorEnum>() {\n                                                Some(gio::IOErrorEnum::FailedHandled) => Ok(false),\n                                                _ => Err(format!(\"{err}\"))\n                                            }}\n                                        }));\n                                    },\n                                );\n                                break;\n                            }\n                        }\n                        Cmd::NetworkDrive(uri, result_tx) => {\n                            let file = gio::File::for_uri(&uri);\n                            let mount_op = mount_op(uri.clone(), event_tx.clone());\n                            let event_tx = event_tx.clone();\n                            file.mount_enclosing_volume(\n                                gio::MountMountFlags::NONE,\n                                Some(&mount_op),\n                                gio::Cancellable::NONE,\n                                move |res| {\n                                    log::info!(\"network drive {uri}: result {res:?}\");\n                                    let Some(event_tx) = event_tx.upgrade() else {\n                                        return;\n                                    };\n                                    event_tx.send(Event::NetworkResult(uri, match res {\n                                        Ok(()) => {\n                                            _ = result_tx.send(Ok(()));\n                                            Ok(true)},\n                                        Err(err) => {\n                                            _ = result_tx.send(Err(anyhow::anyhow!(\"{err:?}\")));\n                                            match err.kind::<gio::IOErrorEnum>() {\n                                            Some(gio::IOErrorEnum::FailedHandled) => Ok(false),\n                                            _ => Err(format!(\"{err}\"))\n                                        }}\n                                    }));\n                                }\n                            );\n                        }\n                        Cmd::NetworkScan(uri, sizes, items_tx) => {\n                            let (resolved_uri, file) = resolve_uri(&uri);\n\n                            let needs_mount = resolved_uri != \"network:///\" && match file.find_enclosing_mount(gio::Cancellable::NONE) {\n                                Ok(_) => false,\n                                Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))\n                            };\n\n                            if needs_mount {\n                                let mount_op = mount_op(resolved_uri.clone(), event_tx.clone());\n                                let event_tx = event_tx.clone();\n                                file.mount_enclosing_volume(\n                                    gio::MountMountFlags::empty(),\n                                    Some(&mount_op),\n                                    gio::Cancellable::NONE,\n                                    move |res| {\n                                        log::info!(\"network scan mounted {resolved_uri}: result {res:?}\");\n                                        // FIXME sometimes a uri can be mounted and then not recognized as mounted...\n                                        // seems to be related to uri with a path\n                                        items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();\n                                        let Some(event_tx) = event_tx.upgrade() else {\n                                            return;\n                                        };\n                                        event_tx.send(Event::NetworkResult(resolved_uri, match res {\n                                            Ok(()) => {\n                                                Ok(true)\n                                            },\n                                            Err(err) => match err.kind::<gio::IOErrorEnum>() {\n                                                Some(gio::IOErrorEnum::FailedHandled) => Ok(false),\n                                                _ => Err(format!(\"{err}\"))\n                                            }\n                                        }));\n                                    }\n                                );\n                            } else {\n                                items_tx.send(network_scan(&uri, sizes)).await.unwrap();\n                            }\n                        }\n                        Cmd::DirInfo(uri, result_tx) => {\n                            result_tx.send(dir_info(&uri)).await.unwrap();\n                        }\n                        Cmd::Unmount(mounter_item) => {\n                            let MounterItem::Gvfs(item) = mounter_item else { continue };\n                            let ItemKind::Mount = item.kind else { continue };\n                            for (i, mount) in monitor.mounts().into_iter().enumerate() {\n                                if i != item.index {\n                                    continue;\n                                }\n\n                                let name = MountExt::name(&mount);\n                                if item.name != name {\n                                    log::warn!(\"trying to unmount mount {} failed: name is {:?} when {:?} was expected\", i, name, item.name);\n                                    continue;\n                                }\n\n                                if MountExt::can_eject(&mount) {\n                                    log::info!(\"eject {name}\");\n                                    MountExt::eject_with_operation(\n                                        &mount,\n                                        gio::MountUnmountFlags::NONE,\n                                        gio::MountOperation::NONE,\n                                        gio::Cancellable::NONE,\n                                        move |result| {\n                                            log::info!(\"eject {name}: result {result:?}\");\n                                        },\n                                    );\n                                } else {\n                                    log::info!(\"unmount {name}\");\n                                    MountExt::unmount_with_operation(\n                                        &mount,\n                                        gio::MountUnmountFlags::NONE,\n                                        gio::MountOperation::NONE,\n                                        gio::Cancellable::NONE,\n                                        move |result| {\n                                            log::info!(\"unmount {name}: result {result:?}\");\n                                        },\n                                    );\n                                }\n                            }\n                        }\n                    }\n                }\n            });\n            main_loop.run();\n        });\n        Self {\n            command_tx,\n            event_rx: Arc::new(event_rx),\n        }\n    }\n}\n\nimpl Mounter for Gvfs {\n    fn items(&self, sizes: IconSizes) -> Option<MounterItems> {\n        let (items_tx, mut items_rx) = mpsc::channel(1);\n        self.command_tx.send(Cmd::Items(sizes, items_tx)).unwrap();\n        items_rx.blocking_recv()\n    }\n\n    fn mount(&self, item: MounterItem) -> Task<()> {\n        let command_tx = self.command_tx.clone();\n        Task::perform(\n            async move {\n                let (res_tx, res_rx) = tokio::sync::oneshot::channel();\n\n                command_tx.send(Cmd::Mount(item, res_tx)).unwrap();\n                res_rx.await\n            },\n            |x| {\n                if let Err(err) = x {\n                    log::error!(\"{err:?}\");\n                }\n            },\n        )\n    }\n\n    fn network_drive(&self, uri: String) -> Task<()> {\n        let command_tx = self.command_tx.clone();\n        Task::perform(\n            async move {\n                let (res_tx, res_rx) = tokio::sync::oneshot::channel();\n\n                command_tx.send(Cmd::NetworkDrive(uri, res_tx)).unwrap();\n                res_rx.await\n            },\n            |x| {\n                if let Err(err) = x {\n                    log::error!(\"{err:?}\");\n                }\n            },\n        )\n    }\n\n    fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {\n        let (items_tx, mut items_rx) = mpsc::channel(1);\n        self.command_tx\n            .send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))\n            .unwrap();\n        items_rx.blocking_recv()\n    }\n\n    fn dir_info(&self, uri: &str) -> Option<(String, String, Option<PathBuf>)> {\n        let (result_tx, mut result_rx) = mpsc::channel(1);\n        self.command_tx\n            .send(Cmd::DirInfo(uri.to_string(), result_tx))\n            .unwrap();\n        result_rx.blocking_recv().and_then(|res| res.ok())\n    }\n\n    fn unmount(&self, item: MounterItem) -> Task<()> {\n        let command_tx = self.command_tx.clone();\n        Task::future(async move {\n            command_tx.send(Cmd::Unmount(item)).unwrap();\n        })\n    }\n\n    fn subscription(&self) -> Subscription<MounterMessage> {\n        let command_tx = self.command_tx.clone();\n        let event_rx = self.event_rx.clone();\n        struct Wrapper {\n            command_tx: mpsc::UnboundedSender<Cmd>,\n            event_rx: Arc<crate::channel::Receiver<Event>>,\n        }\n        impl Hash for Wrapper {\n            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n                TypeId::of::<Self>().hash(state);\n            }\n        }\n        Subscription::run_with(\n            Wrapper {\n                command_tx,\n                event_rx,\n            },\n            |Wrapper {\n                 command_tx,\n                 event_rx,\n             }| {\n                let command_tx = command_tx.clone();\n                let event_rx = event_rx.clone();\n                stream::channel(\n                    1,\n                    move |mut output: cosmic::iced::futures::channel::mpsc::Sender<\n                        MounterMessage,\n                    >| async move {\n                        command_tx.send(Cmd::Rescan).unwrap();\n                        while let Some(event) = event_rx.recv().await {\n                            match event {\n                                Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),\n                                Event::Items(items) => {\n                                    output.send(MounterMessage::Items(items)).await.unwrap();\n                                }\n                                Event::MountResult(item, res) => output\n                                    .send(MounterMessage::MountResult(item, res))\n                                    .await\n                                    .unwrap(),\n                                Event::NetworkAuth(uri, auth, auth_tx) => output\n                                    .send(MounterMessage::NetworkAuth(uri, auth, auth_tx))\n                                    .await\n                                    .unwrap(),\n                                Event::NetworkResult(uri, res) => output\n                                    .send(MounterMessage::NetworkResult(uri, res))\n                                    .await\n                                    .unwrap(),\n                            }\n                        }\n                        pending().await\n                    },\n                )\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "src/mounter/mod.rs",
    "content": "use cosmic::iced::Subscription;\nuse cosmic::{Task, widget};\nuse std::collections::BTreeMap;\nuse std::fmt;\nuse std::path::PathBuf;\nuse std::sync::{Arc, LazyLock};\nuse tokio::sync::mpsc;\n\nuse crate::config::IconSizes;\nuse crate::tab;\n\n#[cfg(feature = \"gvfs\")]\nmod gvfs;\n\n#[derive(Clone)]\npub struct MounterAuth {\n    pub message: String,\n    pub username_opt: Option<String>,\n    pub domain_opt: Option<String>,\n    pub password_opt: Option<String>,\n    pub remember_opt: Option<bool>,\n    pub anonymous_opt: Option<bool>,\n}\n\n// Custom debug for MounterAuth to hide password\nimpl fmt::Debug for MounterAuth {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"MounterAuth\")\n            .field(\"username_opt\", &self.username_opt)\n            .field(\"domain_opt\", &self.domain_opt)\n            .field(\n                \"password_opt\",\n                if self.password_opt.is_some() {\n                    &\"Some(*)\"\n                } else {\n                    &\"None\"\n                },\n            )\n            .field(\"remember_opt\", &self.remember_opt)\n            .field(\"anonymous_opt\", &self.anonymous_opt)\n            .finish()\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum MounterItem {\n    #[cfg(feature = \"gvfs\")]\n    Gvfs(gvfs::Item),\n    #[allow(dead_code)]\n    None,\n}\n\nimpl MounterItem {\n    pub fn name(&self) -> String {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.name(),\n            Self::None => unreachable!(),\n        }\n    }\n\n    pub fn uri(&self) -> String {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.uri(),\n            Self::None => unreachable!(),\n        }\n    }\n\n    pub fn is_mounted(&self) -> bool {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.is_mounted(),\n            Self::None => unreachable!(),\n        }\n    }\n\n    pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.icon(symbolic),\n            Self::None => unreachable!(),\n        }\n    }\n\n    pub fn path(&self) -> Option<PathBuf> {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.path(),\n            Self::None => unreachable!(),\n        }\n    }\n\n    pub fn is_remote(&self) -> bool {\n        match self {\n            #[cfg(feature = \"gvfs\")]\n            Self::Gvfs(item) => item.is_remote(),\n            Self::None => unreachable!(),\n        }\n    }\n}\n\npub type MounterItems = Vec<MounterItem>;\n\n#[derive(Clone, Debug)]\npub enum MounterMessage {\n    Items(MounterItems),\n    MountResult(MounterItem, Result<bool, String>),\n    NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),\n    NetworkResult(String, Result<bool, String>),\n}\n\npub trait Mounter: Send + Sync {\n    fn items(&self, sizes: IconSizes) -> Option<MounterItems>;\n    //TODO: send result\n    fn mount(&self, item: MounterItem) -> Task<()>;\n    fn network_drive(&self, uri: String) -> Task<()>;\n    fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;\n    fn dir_info(&self, uri: &str) -> Option<(String, String, Option<PathBuf>)>;\n    fn unmount(&self, item: MounterItem) -> Task<()>;\n    fn subscription(&self) -> Subscription<MounterMessage>;\n}\n\n#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]\npub struct MounterKey(pub &'static str);\npub type MounterMap = BTreeMap<MounterKey, Box<dyn Mounter>>;\npub type Mounters = Arc<MounterMap>;\n\npub fn mounters() -> Mounters {\n    #[allow(unused_mut)]\n    let mut mounters = MounterMap::new();\n\n    #[cfg(feature = \"gvfs\")]\n    {\n        mounters.insert(MounterKey(\"gvfs\"), Box::new(gvfs::Gvfs::new()));\n    }\n\n    Mounters::new(mounters)\n}\n\npub static MOUNTERS: LazyLock<Mounters> = LazyLock::new(mounters);\n"
  },
  {
    "path": "src/mouse_area.rs",
    "content": "//! A container for capturing mouse events.\n\nuse std::time::Instant;\n\nuse crate::tab::DOUBLE_CLICK_DURATION;\nuse cosmic::iced::core::border::Border;\nuse cosmic::iced::core::event::Event;\nuse cosmic::iced::core::mouse::{self, click};\nuse cosmic::iced::core::renderer::{self, Quad, Renderer as _};\nuse cosmic::iced::core::widget::{Operation, Tree, tree};\nuse cosmic::iced::core::{\n    Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout,\n    overlay, touch,\n};\nuse cosmic::widget::Id;\nuse cosmic::{Element, Renderer, Theme};\n\n/// Emit messages on mouse events.\n#[allow(missing_debug_implementations)]\npub struct MouseArea<'a, Message> {\n    id: Id,\n    content: Element<'a, Message>,\n    on_auto_scroll: Option<Box<dyn OnAutoScroll<'a, Message>>>,\n    on_drag: Option<Box<dyn OnDrag<'a, Message>>>,\n    on_double_click: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_press: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_drag_end: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_release: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_resize: Option<Box<dyn OnResize<'a, Message>>>,\n    on_right_press: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_right_press_no_capture: bool,\n    on_right_press_window_position: bool,\n    on_right_release: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_middle_press: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_middle_release: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_back_press: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_back_release: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_forward_press: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_forward_release: Option<Box<dyn OnMouseButton<'a, Message>>>,\n    on_scroll: Option<Box<dyn OnScroll<'a, Message>>>,\n    on_enter: Option<Box<dyn OnEnterExit<'a, Message>>>,\n    on_exit: Option<Box<dyn OnEnterExit<'a, Message>>>,\n    show_drag_rect: bool,\n}\n\nimpl<'a, Message> MouseArea<'a, Message> {\n    /// The message to emit when auto scroll changes.\n    #[must_use]\n    pub fn on_auto_scroll(mut self, message: impl OnAutoScroll<'a, Message>) -> Self {\n        self.on_auto_scroll = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit when a drag is initiated.\n    #[must_use]\n    pub fn on_drag(mut self, message: impl OnDrag<'a, Message>) -> Self {\n        self.on_drag = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit when a drag ends.\n    #[must_use]\n    pub fn on_drag_end(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_drag_end = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a double click.\n    #[must_use]\n    pub fn on_double_click(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_double_click = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a left button press.\n    #[must_use]\n    pub fn on_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_press = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a left button release.\n    #[must_use]\n    pub fn on_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_release = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on resizing.\n    #[must_use]\n    pub fn on_resize(mut self, message: impl OnResize<'a, Message>) -> Self {\n        self.on_resize = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a right button press.\n    #[must_use]\n    pub fn on_right_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_right_press = Some(Box::new(message));\n        self\n    }\n\n    /// on_right_press will not capture input\n    #[must_use]\n    pub fn on_right_press_no_capture(mut self) -> Self {\n        self.on_right_press_no_capture = true;\n        self\n    }\n\n    /// Only on wayland, on_right_press will provide window position instead of widget relative\n    #[must_use]\n    pub fn wayland_on_right_press_window_position(mut self) -> Self {\n        #[cfg(feature = \"wayland\")]\n        {\n            self.on_right_press_window_position = true;\n        }\n        self\n    }\n\n    /// on_right_press will provide window position instead of widget relative\n    #[must_use]\n    pub fn on_right_press_window_position(mut self) -> Self {\n        self.on_right_press_window_position = true;\n        self\n    }\n\n    /// The message to emit on a right button release.\n    #[must_use]\n    pub fn on_right_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_right_release = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a middle button press.\n    #[must_use]\n    pub fn on_middle_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_middle_press = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a middle button release.\n    #[must_use]\n    pub fn on_middle_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_middle_release = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a back button press.\n    #[must_use]\n    pub fn on_back_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_back_press = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a back button release.\n    #[must_use]\n    pub fn on_back_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_back_release = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a forward button press.\n    #[must_use]\n    pub fn on_forward_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_forward_press = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a forward button release.\n    #[must_use]\n    pub fn on_forward_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {\n        self.on_forward_release = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit on a scroll.\n    #[must_use]\n    pub fn on_scroll(mut self, message: impl OnScroll<'a, Message>) -> Self {\n        self.on_scroll = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit when a mouse enters the area.\n    #[must_use]\n    pub fn on_enter(mut self, message: impl OnEnterExit<'a, Message>) -> Self {\n        self.on_enter = Some(Box::new(message));\n        self\n    }\n\n    /// The message to emit when a mouse exits the area.\n    #[must_use]\n    pub fn on_exit(mut self, message: impl OnEnterExit<'a, Message>) -> Self {\n        self.on_exit = Some(Box::new(message));\n        self\n    }\n\n    #[must_use]\n    pub const fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {\n        self.show_drag_rect = show_drag_rect;\n        self\n    }\n\n    /// Sets the widget's unique identifier.\n    #[must_use]\n    pub fn with_id(mut self, id: Id) -> Self {\n        self.id = id;\n        self\n    }\n}\n\npub trait OnAutoScroll<'a, Message>: Fn(Option<f32>) -> Message + 'a {}\nimpl<'a, Message, F> OnAutoScroll<'a, Message> for F where F: Fn(Option<f32>) -> Message + 'a {}\n\npub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {}\nimpl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {}\n\npub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Message + 'a {}\nimpl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Message + 'a {}\n\npub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {}\nimpl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {}\n\npub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta) -> Option<Message> + 'a {}\nimpl<'a, Message, F> OnScroll<'a, Message> for F where\n    F: Fn(mouse::ScrollDelta) -> Option<Message> + 'a\n{\n}\n\npub trait OnEnterExit<'a, Message>: Fn() -> Message + 'a {}\nimpl<'a, Message, F> OnEnterExit<'a, Message> for F where F: Fn() -> Message + 'a {}\n\n/// Local state of the [`MouseArea`].\n#[derive(Default)]\nstruct State {\n    last_auto_scroll: Option<f32>,\n    last_position: Option<Point>,\n    last_virtual_position: Option<Point>,\n    drag_initiated: Option<Point>,\n    prev_click: Option<(mouse::Click, Instant)>,\n    viewport: Option<Rectangle>,\n}\n\nimpl State {\n    fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> {\n        if let Some(drag_source) = self.drag_initiated\n            && let Some(position) = cursor.position().or(self.last_virtual_position)\n            && position.distance(drag_source) > 1.0\n        {\n            let min_x = drag_source.x.min(position.x);\n            let max_x = drag_source.x.max(position.x);\n            let min_y = drag_source.y.min(position.y);\n            let max_y = drag_source.y.max(position.y);\n            return Some(Rectangle::new(\n                Point::new(min_x, min_y),\n                Size::new(max_x - min_x, max_y - min_y),\n            ));\n        }\n        None\n    }\n\n    fn click(&mut self, pos: Point) -> mouse::Click {\n        let now = Instant::now();\n\n        let new = if let Some((prev_click, prev_time)) = self.prev_click.take() {\n            if now.duration_since(prev_time) < DOUBLE_CLICK_DURATION {\n                match prev_click.kind() {\n                    mouse::click::Kind::Single => {\n                        mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))\n                    }\n                    mouse::click::Kind::Double => {\n                        mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))\n                    }\n                    mouse::click::Kind::Triple => {\n                        mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))\n                    }\n                }\n            } else {\n                mouse::Click::new(pos, mouse::Button::Left, None)\n            }\n        } else {\n            mouse::Click::new(pos, mouse::Button::Left, None)\n        };\n        self.prev_click = Some((new, now));\n        new\n    }\n}\n\nimpl<'a, Message> MouseArea<'a, Message> {\n    /// Creates a [`MouseArea`] with the given content.\n    pub fn new(content: impl Into<Element<'a, Message>>) -> Self {\n        MouseArea {\n            id: Id::unique(),\n            content: content.into(),\n            on_auto_scroll: None,\n            on_drag: None,\n            on_drag_end: None,\n            on_double_click: None,\n            on_press: None,\n            on_release: None,\n            on_resize: None,\n            on_right_press: None,\n            on_right_press_no_capture: false,\n            on_right_press_window_position: false,\n            on_right_release: None,\n            on_middle_press: None,\n            on_middle_release: None,\n            on_back_press: None,\n            on_back_release: None,\n            on_forward_press: None,\n            on_forward_release: None,\n            on_enter: None,\n            on_exit: None,\n            on_scroll: None,\n            show_drag_rect: false,\n        }\n    }\n}\n\nimpl<Message> Widget<Message, Theme, Renderer> for MouseArea<'_, Message>\nwhere\n    Message: Clone,\n{\n    fn tag(&self) -> tree::Tag {\n        tree::Tag::of::<State>()\n    }\n\n    fn state(&self) -> tree::State {\n        tree::State::new(State::default())\n    }\n\n    fn children(&self) -> Vec<Tree> {\n        vec![Tree::new(&self.content)]\n    }\n\n    fn diff(&mut self, tree: &mut Tree) {\n        tree.diff_children(std::slice::from_mut(&mut self.content));\n    }\n\n    fn size(&self) -> Size<Length> {\n        self.content.as_widget().size()\n    }\n\n    fn layout(\n        &mut self,\n        tree: &mut Tree,\n        renderer: &Renderer,\n        limits: &layout::Limits,\n    ) -> layout::Node {\n        self.content\n            .as_widget_mut()\n            .layout(&mut tree.children[0], renderer, limits)\n    }\n\n    fn operate(\n        &mut self,\n        tree: &mut Tree,\n        layout: Layout<'_>,\n        renderer: &Renderer,\n        operation: &mut dyn Operation,\n    ) {\n        self.content\n            .as_widget_mut()\n            .operate(&mut tree.children[0], layout, renderer, operation);\n    }\n\n    fn update(\n        &mut self,\n        tree: &mut Tree,\n        event: &Event,\n        layout: Layout<'_>,\n        cursor: mouse::Cursor,\n        renderer: &Renderer,\n        clipboard: &mut dyn Clipboard,\n        shell: &mut Shell<'_, Message>,\n        viewport: &Rectangle,\n    ) {\n        self.content.as_widget_mut().update(\n            &mut tree.children[0],\n            event,\n            layout,\n            cursor,\n            renderer,\n            clipboard,\n            shell,\n            viewport,\n        );\n\n        if shell.is_event_captured() {\n            return;\n        }\n\n        update(\n            self,\n            event,\n            layout,\n            cursor,\n            shell,\n            tree.state.downcast_mut::<State>(),\n            viewport,\n        );\n    }\n\n    fn mouse_interaction(\n        &self,\n        tree: &Tree,\n        layout: Layout<'_>,\n        cursor: mouse::Cursor,\n        viewport: &Rectangle,\n        renderer: &Renderer,\n    ) -> mouse::Interaction {\n        self.content.as_widget().mouse_interaction(\n            &tree.children[0],\n            layout,\n            cursor,\n            viewport,\n            renderer,\n        )\n    }\n\n    fn draw(\n        &self,\n        tree: &Tree,\n        renderer: &mut Renderer,\n        theme: &Theme,\n        renderer_style: &renderer::Style,\n        layout: Layout<'_>,\n        cursor: mouse::Cursor,\n        viewport: &Rectangle,\n    ) {\n        self.content.as_widget().draw(\n            &tree.children[0],\n            renderer,\n            theme,\n            renderer_style,\n            layout,\n            cursor,\n            viewport,\n        );\n\n        if self.show_drag_rect {\n            let state = tree.state.downcast_ref::<State>();\n            if let Some(bounds) = state.drag_rect(cursor) {\n                let cosmic = theme.cosmic();\n                let mut bg_color = cosmic.accent_color();\n                //TODO: get correct alpha\n                bg_color.alpha = 0.2;\n                renderer.start_layer(*viewport);\n                renderer.fill_quad(\n                    Quad {\n                        bounds,\n                        border: Border {\n                            color: cosmic.accent_color().into(),\n                            width: 1.0,\n                            radius: cosmic.radius_xs().into(),\n                        },\n                        ..Default::default()\n                    },\n                    Color::from(bg_color),\n                );\n                renderer.end_layer();\n            }\n        }\n    }\n\n    fn overlay<'b>(\n        &'b mut self,\n        tree: &'b mut Tree,\n        layout: Layout<'b>,\n        renderer: &Renderer,\n        viewport: &Rectangle,\n        translation: Vector,\n    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {\n        self.content.as_widget_mut().overlay(\n            &mut tree.children[0],\n            layout,\n            renderer,\n            viewport,\n            translation,\n        )\n    }\n\n    fn drag_destinations(\n        &self,\n        state: &Tree,\n        layout: Layout<'_>,\n        renderer: &Renderer,\n        dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles,\n    ) {\n        self.content.as_widget().drag_destinations(\n            &state.children[0],\n            layout,\n            renderer,\n            dnd_rectangles,\n        );\n    }\n\n    fn id(&self) -> Option<Id> {\n        Some(self.id.clone())\n    }\n\n    fn set_id(&mut self, id: Id) {\n        self.id = id;\n    }\n}\n\nimpl<'a, Message> From<MouseArea<'a, Message>> for Element<'a, Message>\nwhere\n    Message: 'a + Clone,\n    Renderer: 'a + renderer::Renderer,\n    Theme: 'a,\n{\n    fn from(area: MouseArea<'a, Message>) -> Self {\n        Element::new(area)\n    }\n}\n\n/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`]\n/// accordingly.\nfn update<Message: Clone>(\n    widget: &mut MouseArea<'_, Message>,\n    event: &Event,\n    layout: Layout<'_>,\n    cursor: mouse::Cursor,\n    shell: &mut Shell<'_, Message>,\n    state: &mut State,\n    viewport: &Rectangle,\n) {\n    let offset = layout.virtual_offset();\n    let layout_bounds = layout.bounds();\n\n    let viewport_changed = state.viewport != Some(*viewport);\n\n    if let Some(message) = widget.on_resize.as_ref()\n        && viewport_changed\n    {\n        shell.publish(message(*viewport));\n    }\n\n    state.viewport = Some(*viewport);\n\n    let should_check_hover = viewport_changed\n        || matches!(\n            event,\n            Event::Mouse(mouse::Event::CursorMoved { .. })\n                | Event::Mouse(mouse::Event::WheelScrolled { .. })\n        );\n\n    if should_check_hover {\n        let position_in = cursor.position_in(layout_bounds);\n        match (position_in, state.last_position) {\n            (None, Some(_)) => {\n                if let Some(message) = widget.on_exit.as_ref() {\n                    shell.publish(message());\n                }\n            }\n            (Some(_), None) => {\n                if let Some(message) = widget.on_enter.as_ref() {\n                    shell.publish(message());\n                }\n            }\n            _ => {}\n        }\n        state.last_position = position_in;\n    }\n\n    if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {\n        let virtual_position = Point::new(\n            viewport.x - layout_bounds.x + position.x,\n            viewport.y - layout_bounds.y + position.y,\n        );\n        state.last_virtual_position = Some(virtual_position);\n\n        if let Some(message) = widget.on_auto_scroll.as_ref() {\n            let auto_scroll = if state.drag_initiated.is_some() {\n                let bottom = viewport.y;\n                let top = viewport.y + viewport.height;\n                if virtual_position.y < bottom {\n                    Some(virtual_position.y - bottom)\n                } else if virtual_position.y > top {\n                    Some(virtual_position.y - top)\n                } else {\n                    None\n                }\n            } else {\n                None\n            };\n            if state.last_auto_scroll != auto_scroll {\n                shell.publish(message(auto_scroll));\n                state.last_auto_scroll = auto_scroll;\n            }\n        }\n    }\n\n    if state.drag_initiated.is_none() && !cursor.is_over(layout_bounds) {\n        return;\n    }\n\n    if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))\n    | Event::Touch(touch::Event::FingerPressed { .. }) = event\n    {\n        let click = state.click(cursor.position_in(layout_bounds).unwrap_or_default());\n        match click.kind() {\n            click::Kind::Single => {\n                if let Some(message) = widget.on_press.as_ref() {\n                    shell.publish(message(cursor.position_in(layout_bounds)));\n                }\n            }\n            click::Kind::Double => {\n                if let Some(message) = widget.on_double_click.as_ref() {\n                    shell.publish(message(cursor.position_in(layout_bounds)));\n                }\n            }\n            click::Kind::Triple => {\n                // TODO what to do here\n                if let Some(message) = widget.on_press.as_ref() {\n                    shell.publish(message(cursor.position_in(layout_bounds)));\n                }\n            }\n        }\n        if widget.on_drag.is_some() {\n            state.drag_initiated = cursor.position();\n        }\n\n        if widget.on_press.is_some() {\n            shell.capture_event();\n            return;\n        }\n    }\n\n    let distance_dragged = state\n        .drag_initiated\n        .map(|initiated| initiated.distance(cursor.position().unwrap_or_default()))\n        .unwrap_or_default();\n    if matches!(\n        event,\n        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))\n            | Event::Touch(touch::Event::FingerLifted { .. })\n    ) && distance_dragged > 1.0\n    {\n        state.drag_initiated = None;\n        state.prev_click = None;\n        if let Some(message) = widget.on_drag_end.as_ref() {\n            shell.publish(message(cursor.position_in(layout_bounds)));\n        }\n    }\n\n    let recent_click = state\n        .prev_click\n        .as_ref()\n        .is_some_and(|(_, i)| Instant::now().duration_since(*i) <= DOUBLE_CLICK_DURATION);\n    if matches!(\n        event,\n        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))\n            | Event::Touch(touch::Event::FingerLifted { .. })\n    ) && state.prev_click.is_some()\n    {\n        if !recent_click {\n            state.prev_click = None;\n            return;\n        }\n        state.drag_initiated = None;\n        if let Some(message) = widget.on_release.as_ref() {\n            shell.publish(message(cursor.position_in(layout_bounds)));\n\n            shell.capture_event();\n            return;\n        }\n    }\n\n    if let Some(message) = widget.on_right_press.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))\n        )\n    {\n        let point_opt = if widget.on_right_press_window_position {\n            cursor.position_over(layout_bounds).map(|mut p| {\n                p.x -= offset.x;\n                p.y -= offset.y;\n                p\n            })\n        } else {\n            cursor.position_in(layout_bounds)\n        };\n        shell.publish(message(point_opt));\n\n        if widget.on_right_press_no_capture {\n            return;\n        }\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_right_release.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_middle_press.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_middle_release.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_back_press.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_back_release.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_forward_press.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(message) = widget.on_forward_release.as_ref()\n        && matches!(\n            event,\n            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward))\n        )\n    {\n        shell.publish(message(cursor.position_in(layout_bounds)));\n\n        shell.capture_event();\n        return;\n    }\n\n    if let Some(on_scroll) = widget.on_scroll.as_ref()\n        && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event\n        && let Some(message) = on_scroll(*delta)\n    {\n        shell.publish(message);\n        shell.capture_event();\n        return;\n    }\n\n    if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {\n        shell.publish(message(drag_rect.intersection(&layout_bounds).map(\n            |mut rect| {\n                rect.x -= layout_bounds.x;\n                rect.y -= layout_bounds.y;\n                rect\n            },\n        )));\n    }\n}\n"
  },
  {
    "path": "src/operation/controller.rs",
    "content": "use atomic_float::AtomicF32;\nuse num_enum::{IntoPrimitive, TryFromPrimitive};\nuse std::sync::Arc;\nuse std::sync::atomic::{self, AtomicU16};\nuse tokio::sync::Notify;\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]\n#[repr(u16)]\npub enum ControllerState {\n    Cancelled,\n    Failed,\n    Paused,\n    Running,\n}\n\n#[derive(Debug)]\nstruct ControllerInner {\n    state: AtomicU16,\n    progress: AtomicF32,\n    notify: Notify,\n}\n\n#[derive(Debug)]\npub struct Controller {\n    primary: bool,\n    inner: Arc<ControllerInner>,\n}\n\nimpl Default for Controller {\n    fn default() -> Self {\n        Self {\n            primary: true,\n            inner: Arc::new(ControllerInner {\n                state: AtomicU16::new(ControllerState::Running.into()),\n                progress: AtomicF32::new(0.0),\n                notify: Notify::new(),\n            }),\n        }\n    }\n}\n\nimpl Controller {\n    pub async fn check(&self) -> Result<(), ControllerState> {\n        loop {\n            match self.state() {\n                ControllerState::Cancelled => return Err(ControllerState::Cancelled),\n                ControllerState::Failed => return Err(ControllerState::Failed),\n                ControllerState::Paused => (),\n                ControllerState::Running => return Ok(()),\n            }\n\n            self.inner.notify.notified().await;\n        }\n    }\n\n    pub fn progress(&self) -> f32 {\n        self.inner.progress.load(atomic::Ordering::Relaxed)\n    }\n\n    pub fn set_progress(&self, progress: f32) {\n        self.inner\n            .progress\n            .swap(progress, atomic::Ordering::Relaxed);\n    }\n\n    pub fn state(&self) -> ControllerState {\n        ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed))\n            .unwrap_or(ControllerState::Failed)\n    }\n\n    pub fn set_state(&self, state: ControllerState) {\n        self.inner\n            .state\n            .store(state.into(), atomic::Ordering::Relaxed);\n        self.inner.notify.notify_waiters();\n    }\n\n    pub fn is_cancelled(&self) -> bool {\n        matches!(self.state(), ControllerState::Cancelled)\n    }\n\n    pub fn cancel(&self) {\n        self.set_state(ControllerState::Cancelled);\n    }\n\n    pub fn is_failed(&self) -> bool {\n        matches!(self.state(), ControllerState::Failed)\n    }\n\n    pub fn is_paused(&self) -> bool {\n        matches!(self.state(), ControllerState::Paused)\n    }\n\n    pub fn pause(&self) {\n        self.set_state(ControllerState::Paused);\n    }\n\n    /// Returns when the state is paused.\n    ///\n    /// Use this to pause futures.\n    pub async fn until_paused(&self) {\n        loop {\n            if matches!(self.state(), ControllerState::Paused) {\n                return;\n            }\n\n            self.inner.notify.notified().await;\n        }\n    }\n\n    /// Returns when state is neither paused, cancelled, nor failed.\n    ///\n    /// Use this to resume futures.\n    pub async fn until_unpaused(&self) {\n        loop {\n            if !matches!(\n                self.state(),\n                ControllerState::Paused | ControllerState::Cancelled | ControllerState::Failed\n            ) {\n                return;\n            }\n\n            self.inner.notify.notified().await;\n        }\n    }\n\n    pub fn unpause(&self) {\n        if !self.is_cancelled() | !self.is_failed() {\n            self.set_state(ControllerState::Running);\n        }\n    }\n}\n\nimpl Clone for Controller {\n    fn clone(&self) -> Self {\n        Self {\n            primary: false,\n            inner: self.inner.clone(),\n        }\n    }\n}\n\nimpl Drop for Controller {\n    fn drop(&mut self) {\n        // Cancel operations if primary controller is dropped and controller is still running\n        if self.primary && self.state() != ControllerState::Failed {\n            self.cancel();\n        }\n    }\n}\n"
  },
  {
    "path": "src/operation/mod.rs",
    "content": "use crate::app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID};\nuse crate::config::IconSizes;\nuse crate::spawn_detached::spawn_detached;\nuse crate::{archive, fl, tab};\nuse cosmic::iced::futures::channel::mpsc::Sender;\nuse cosmic::iced::futures::{self, SinkExt, StreamExt, stream};\nuse std::borrow::Cow;\nuse std::fmt::Formatter;\nuse std::fs;\nuse std::io::{self, Read, Write};\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse tokio::sync::{Mutex as TokioMutex, mpsc};\nuse walkdir::WalkDir;\nuse zip::AesMode::Aes256;\n\npub use self::controller::{Controller, ControllerState};\npub mod controller;\n\npub use notifiers::*;\nmod notifiers;\n\npub use self::reader::OpReader;\npub mod reader;\n\nuse self::recursive::{Context, Method};\npub mod recursive;\n\nasync fn handle_replace(\n    msg_tx: Arc<TokioMutex<Sender<Message>>>,\n    file_from: PathBuf,\n    file_to: PathBuf,\n    multiple: bool,\n    conflict_count: usize,\n) -> ReplaceResult {\n    let item_from = match tab::item_from_path(file_from, IconSizes::default()) {\n        Ok(ok) => Box::new(ok),\n        Err(err) => {\n            log::warn!(\"{err}\");\n            return ReplaceResult::Cancel;\n        }\n    };\n\n    let item_to = match tab::item_from_path(file_to, IconSizes::default()) {\n        Ok(ok) => Box::new(ok),\n        Err(err) => {\n            log::warn!(\"{err}\");\n            return ReplaceResult::Cancel;\n        }\n    };\n\n    let (tx, mut rx) = mpsc::channel(1);\n    let _ = msg_tx\n        .lock()\n        .await\n        .send(Message::DialogPush(\n            DialogPage::Replace {\n                from: item_from,\n                to: item_to,\n                multiple,\n                apply_to_all: false,\n                conflict_count,\n                tx,\n            },\n            Some(REPLACE_BUTTON_ID.clone()),\n        ))\n        .await;\n    rx.recv().await.unwrap_or(ReplaceResult::Cancel)\n}\n\nfn get_directory_name(file_name: &str) -> &str {\n    // TODO: Chain with COMPOUND_EXTENSIONS once more formats are supported\n    for ext in crate::archive::SUPPORTED_EXTENSIONS {\n        if let Some(stripped) = file_name.strip_suffix(ext) {\n            return stripped;\n        }\n    }\n    file_name\n}\n\n#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]\npub enum ReplaceResult {\n    Replace(bool),\n    KeepBoth,\n    Skip(bool),\n    Cancel,\n}\n\nasync fn copy_or_move(\n    paths: Vec<PathBuf>,\n    to: PathBuf,\n    method: Method,\n    msg_tx: &Arc<TokioMutex<Sender<Message>>>,\n    controller: Controller,\n) -> Result<OperationSelection, OperationError> {\n    let msg_tx = msg_tx.clone();\n    let controller_c = controller.clone();\n\n    compio::runtime::spawn(async move {\n        let controller = controller_c;\n        log::info!(\n            \"{} {:?} to {}\",\n            match method {\n                Method::Copy => \"Copy\",\n                Method::Move { .. } => \"Move\",\n            },\n            paths,\n            to.display()\n        );\n\n        // Handle duplicate file names by renaming paths\n        let from_to_pairs_iter = paths\n            .into_iter()\n            .zip(std::iter::repeat(to.as_path()))\n            .filter_map(|(from, to)| {\n                if matches!(from.parent(), Some(parent) if parent == to)\n                    && matches!(method, Method::Copy)\n                {\n                    // `from`'s parent is equal to `to` which means we're copying to the same\n                    // directory (duplicating files)\n                    let to = copy_unique_path(&from, to);\n                    Some((from, to))\n                } else if let Some(name) = from.file_name() {\n                    let to = to.join(name);\n                    Some((from, to))\n                } else {\n                    //TODO: how to handle from missing file name?\n                    None\n                }\n            });\n\n        // Attempt quick and simple renames\n        //TODO: allow rename to be used for directories in recursive context?\n\n        let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) {\n            from_to_pairs_iter\n                .map(|(from, to)| async move {\n                    //TODO: show replace dialog here?\n                    if to.exists() {\n                        return Some((from, to));\n                    }\n\n                    match compio::fs::rename(&from, &to).await {\n                        Ok(()) => {\n                            log::info!(\"renamed {} to {}\", from.display(), to.display());\n                            None\n                        }\n                        Err(err) => {\n                            log::info!(\n                                \"failed to rename {} to {}, fallback to recursive move: {}\",\n                                from.display(),\n                                to.display(),\n                                err\n                            );\n                            Some((from, to))\n                        }\n                    }\n                })\n                .collect::<cosmic::iced::futures::stream::FuturesOrdered<_>>()\n                .fold(Vec::new(), |mut pairs, pair| async move {\n                    if let Some(pair) = pair {\n                        pairs.push(pair);\n                    }\n                    pairs\n                })\n                .await\n        } else {\n            from_to_pairs_iter.collect()\n        };\n\n        let mut context = Context::new(controller.clone());\n\n        {\n            let controller = controller.clone();\n            context = context.on_progress(move |_op, progress| {\n                let item_progress = match progress.total_bytes {\n                    Some(total_bytes) => {\n                        if total_bytes == 0 {\n                            1.0\n                        } else {\n                            progress.current_bytes as f32 / total_bytes as f32\n                        }\n                    }\n                    None => 0.0,\n                };\n                let total_progress =\n                    (item_progress + progress.current_ops as f32) / progress.total_ops as f32;\n                controller.set_progress(total_progress);\n            });\n        }\n\n        {\n            let msg_tx = msg_tx.clone();\n            context = context.on_replace(move |op, conflict_count| {\n                let msg_tx = msg_tx.clone();\n                Box::pin(handle_replace(\n                    msg_tx,\n                    op.from.clone(),\n                    op.to.clone(),\n                    true,\n                    conflict_count,\n                ))\n            });\n        }\n\n        context\n            .recursive_copy_or_move(from_to_pairs, method)\n            .await?;\n\n        Result::<OperationSelection, OperationError>::Ok(context.op_sel)\n    })\n    .await\n    .map_err(wrap_compio_spawn_error)?\n}\n\npub async fn sync_to_disk(\n    written_files: Vec<PathBuf>,\n    target_dirs: std::collections::HashSet<PathBuf>,\n) {\n    // Sync files to disk\n    stream::iter(written_files.into_iter().map(|path| async move {\n        if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await {\n            let _ = file.sync_all().await;\n        }\n    }))\n    .buffer_unordered(32)\n    .collect::<()>()\n    .await;\n\n    // Sync directories to disk\n    stream::iter(target_dirs.into_iter().map(|path| async move {\n        if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await {\n            let _ = dir.sync_all().await;\n        }\n    }))\n    .buffer_unordered(16)\n    .collect::<()>()\n    .await;\n}\n\npub fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {\n    // List of compound extensions to check\n    const COMPOUND_EXTENSIONS: &[&str] = &[\n        \".tar.gz\",\n        \".tar.bz2\",\n        \".tar.xz\",\n        \".tar.zst\",\n        \".tar.lz\",\n        \".tar.lzma\",\n        \".tar.sz\",\n        \".tar.lzo\",\n        \".tar.br\",\n        \".tar.Z\",\n        \".tar.pz\",\n    ];\n\n    let mut to = to.to_owned();\n    if let Some(file_name) = from.file_name().and_then(|name| name.to_str()) {\n        let (stem, ext) = if from.is_dir() {\n            (file_name.to_string(), None)\n        } else {\n            let file_name = file_name.to_string();\n            COMPOUND_EXTENSIONS\n                .iter()\n                .copied()\n                .find(|&ext| file_name.ends_with(ext))\n                .map(|ext| {\n                    (\n                        file_name.strip_suffix(ext).unwrap().to_string(),\n                        Some(ext[1..].to_string()),\n                    )\n                })\n                .unwrap_or_else(|| {\n                    from.file_stem()\n                        .and_then(|s| s.to_str())\n                        .map_or((file_name, None), |stem| {\n                            (\n                                stem.to_string(),\n                                from.extension()\n                                    .and_then(|e| e.to_str())\n                                    .map(str::to_string),\n                            )\n                        })\n                })\n        };\n\n        for n in 0.. {\n            let new_name = if n == 0 {\n                file_name.to_string()\n            } else {\n                match ext {\n                    Some(ref ext) => format!(\"{} ({} {}).{}\", stem, fl!(\"copy_noun\"), n, ext),\n                    None => format!(\"{} ({} {})\", stem, fl!(\"copy_noun\"), n),\n                }\n            };\n\n            to.push(&new_name);\n\n            if !matches!(to.try_exists(), Ok(true)) {\n                break;\n            }\n            // Continue if a copy with index exists\n            to.pop();\n        }\n    }\n    to\n}\n\nfn file_name(path: &Path) -> Cow<'_, str> {\n    path.file_name()\n        .map_or_else(|| fl!(\"unknown-folder\").into(), |x| x.to_string_lossy())\n}\n\nfn parent_name(path: &Path) -> Cow<'_, str> {\n    let Some(parent) = path.parent() else {\n        return fl!(\"unknown-folder\").into();\n    };\n\n    file_name(parent)\n}\n\nfn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> {\n    let Some(first_path) = paths.first() else {\n        return fl!(\"unknown-folder\").into();\n    };\n\n    let Some(parent) = first_path.parent() else {\n        return fl!(\"unknown-folder\").into();\n    };\n\n    for path in paths {\n        //TODO: is it possible to have different parents, and what should be returned?\n        if path.parent() != Some(parent) {\n            return fl!(\"unknown-folder\").into();\n        }\n    }\n\n    file_name(parent)\n}\n\n#[derive(Clone, Debug, Default)]\npub struct OperationSelection {\n    // Paths to ignore if they are already selected\n    pub ignored: Vec<PathBuf>,\n    // Paths to select\n    pub selected: Vec<PathBuf>,\n}\n\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum Operation {\n    /// Compress files\n    Compress {\n        paths: Vec<PathBuf>,\n        to: PathBuf,\n        archive_type: ArchiveType,\n        password: Option<String>,\n    },\n    /// Copy items\n    Copy {\n        paths: Vec<PathBuf>,\n        to: PathBuf,\n    },\n    /// Move items to the trash\n    Delete {\n        paths: Vec<PathBuf>,\n    },\n    /// Delete a path from the trash\n    DeleteTrash {\n        items: Vec<trash::TrashItem>,\n    },\n    /// Empty the trash\n    EmptyTrash,\n    /// Uncompress files\n    Extract {\n        paths: Box<[PathBuf]>,\n        to: PathBuf,\n        password: Option<String>,\n    },\n    /// Move items\n    Move {\n        paths: Vec<PathBuf>,\n        to: PathBuf,\n        cross_device_copy: bool,\n    },\n    NewFile {\n        path: PathBuf,\n    },\n    NewFolder {\n        path: PathBuf,\n    },\n    /// Permanently delete items, skipping the trash\n    PermanentlyDelete {\n        paths: Box<[PathBuf]>,\n    },\n    RemoveFromRecents {\n        paths: Box<[PathBuf]>,\n    },\n    Rename {\n        from: PathBuf,\n        to: PathBuf,\n    },\n    /// Restore a path from the trash\n    Restore {\n        items: Vec<trash::TrashItem>,\n    },\n    /// Set executable and launch\n    SetExecutableAndLaunch {\n        path: PathBuf,\n    },\n    /// Set permissions\n    SetPermissions {\n        path: PathBuf,\n        mode: u32,\n    },\n}\n\n#[derive(Clone, Debug)]\npub enum OperationErrorType {\n    Generic(String),\n    PasswordRequired,\n}\n#[derive(Clone, Debug)]\npub struct OperationError {\n    pub kind: OperationErrorType,\n}\n\nimpl OperationError {\n    pub fn from_state(state: ControllerState, controller: &Controller) -> Self {\n        let message = if state == ControllerState::Failed {\n            controller.set_state(ControllerState::Failed);\n            fl!(\"failed\")\n        } else {\n            controller.cancel();\n            fl!(\"cancelled\")\n        };\n\n        Self {\n            kind: OperationErrorType::Generic(message),\n        }\n    }\n\n    pub fn from_err<T: ToString>(err: T, controller: &Controller) -> Self {\n        controller.set_state(ControllerState::Failed);\n\n        Self {\n            kind: OperationErrorType::Generic(err.to_string()),\n        }\n    }\n\n    pub fn from_kind(kind: OperationErrorType, controller: &Controller) -> Self {\n        controller.set_state(ControllerState::Failed);\n        Self { kind }\n    }\n\n    pub fn from_msg(m: impl Into<String>) -> Self {\n        Self {\n            kind: OperationErrorType::Generic(m.into()),\n        }\n    }\n}\n\nimpl std::error::Error for OperationError {}\n\nimpl std::fmt::Display for OperationError {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match &self.kind {\n            OperationErrorType::Generic(s) => s.fmt(f),\n            OperationErrorType::PasswordRequired => f.write_str(\"Password required\"),\n        }\n    }\n}\n\nimpl Operation {\n    pub fn pending_text(&self, ratio: f32, state: ControllerState) -> String {\n        let percent = (ratio * 100.0) as i32;\n        let progress = || match state {\n            ControllerState::Running => fl!(\"progress\", percent = percent),\n            ControllerState::Paused => fl!(\"progress-paused\", percent = percent),\n            ControllerState::Cancelled => fl!(\"progress-cancelled\", percent = percent),\n            ControllerState::Failed => fl!(\"progress-failed\", percent = percent),\n        };\n        match self {\n            Self::Compress { paths, to, .. } => fl!(\n                \"compressing\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to),\n                progress = progress()\n            ),\n            Self::Copy { paths, to } => fl!(\n                \"copying\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to),\n                progress = progress()\n            ),\n            Self::Delete { paths } => fl!(\n                \"moving\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = fl!(\"trash\"),\n                progress = progress()\n            ),\n            Self::DeleteTrash { items } => {\n                fl!(\"deleting\", items = items.len(), progress = progress())\n            }\n            Self::EmptyTrash => fl!(\"emptying-trash\", progress = progress()),\n            Self::Extract {\n                paths,\n                to,\n                password: _,\n            } => fl!(\n                \"extracting\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to),\n                progress = progress()\n            ),\n            Self::Move { paths, to, .. } => fl!(\n                \"moving\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to),\n                progress = progress()\n            ),\n            Self::NewFile { path } => fl!(\n                \"creating\",\n                name = file_name(path),\n                parent = parent_name(path)\n            ),\n            Self::NewFolder { path } => fl!(\n                \"creating\",\n                name = file_name(path),\n                parent = parent_name(path)\n            ),\n            Self::PermanentlyDelete { paths } => fl!(\"permanently-deleting\", items = paths.len()),\n            Self::Rename { from, to } => {\n                fl!(\"renaming\", from = file_name(from), to = file_name(to))\n            }\n            Self::RemoveFromRecents { paths } => fl!(\"removing-from-recents\", items = paths.len()),\n            Self::Restore { items } => fl!(\"restoring\", items = items.len(), progress = progress()),\n            Self::SetExecutableAndLaunch { path } => {\n                fl!(\"setting-executable-and-launching\", name = file_name(path))\n            }\n            Self::SetPermissions { path, mode } => {\n                fl!(\n                    \"setting-permissions\",\n                    name = file_name(path),\n                    mode = format!(\"{:#03o}\", mode)\n                )\n            }\n        }\n    }\n\n    pub fn completed_text(&self) -> String {\n        match self {\n            Self::Compress { paths, to, .. } => fl!(\n                \"compressed\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to)\n            ),\n            Self::Copy { paths, to } => fl!(\n                \"copied\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to)\n            ),\n            Self::Delete { paths } => fl!(\n                \"moved\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = fl!(\"trash\")\n            ),\n            Self::DeleteTrash { items } => fl!(\"deleted\", items = items.len()),\n            Self::EmptyTrash => fl!(\"emptied-trash\"),\n            Self::Extract {\n                paths,\n                to,\n                password: _,\n            } => fl!(\n                \"extracted\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to)\n            ),\n            Self::Move { paths, to, .. } => fl!(\n                \"moved\",\n                items = paths.len(),\n                from = paths_parent_name(paths),\n                to = file_name(to)\n            ),\n            Self::NewFile { path } => fl!(\n                \"created\",\n                name = file_name(path),\n                parent = parent_name(path)\n            ),\n            Self::NewFolder { path } => fl!(\n                \"created\",\n                name = file_name(path),\n                parent = parent_name(path)\n            ),\n            Self::PermanentlyDelete { paths } => fl!(\"permanently-deleted\", items = paths.len()),\n            Self::RemoveFromRecents { paths } => fl!(\"removed-from-recents\", items = paths.len()),\n            Self::Rename { from, to } => fl!(\"renamed\", from = file_name(from), to = file_name(to)),\n            Self::Restore { items } => fl!(\"restored\", items = items.len()),\n            Self::SetExecutableAndLaunch { path } => {\n                fl!(\"set-executable-and-launched\", name = file_name(path))\n            }\n            Self::SetPermissions { path, mode } => {\n                fl!(\n                    \"set-permissions\",\n                    name = file_name(path),\n                    mode = format!(\"{:#03o}\", mode)\n                )\n            }\n        }\n    }\n\n    pub const fn show_progress_notification(&self) -> bool {\n        // Long running operations show a progress notification\n        match self {\n            Self::Compress { .. }\n            | Self::Copy { .. }\n            | Self::Delete { .. }\n            | Self::DeleteTrash { .. }\n            | Self::EmptyTrash\n            | Self::Extract { .. }\n            | Self::Move { .. }\n            | Self::PermanentlyDelete { .. }\n            | Self::Restore { .. } => true,\n            Self::NewFile { .. }\n            | Self::NewFolder { .. }\n            | Self::RemoveFromRecents { .. }\n            | Self::Rename { .. }\n            | Self::SetExecutableAndLaunch { .. }\n            | Self::SetPermissions { .. } => false,\n        }\n    }\n\n    pub fn toast(&self) -> Option<String> {\n        match self {\n            Self::Compress { .. } => Some(self.completed_text()),\n            Self::Delete { .. } => Some(self.completed_text()),\n            Self::Extract { .. } => Some(self.completed_text()),\n            //TODO: more toasts\n            _ => None,\n        }\n    }\n\n    /// Perform the operation\n    pub async fn perform(\n        self,\n        msg_tx: &Arc<TokioMutex<Sender<Message>>>,\n        controller: Controller,\n    ) -> Result<OperationSelection, OperationError> {\n        let controller_clone = controller.clone();\n\n        //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE\n        let paths: Result<OperationSelection, OperationError> = match self {\n            Self::Compress {\n                paths,\n                to,\n                archive_type,\n                password,\n            } => {\n                let controller_c = controller.clone();\n                compio::runtime::spawn_blocking(\n                    move || -> Result<OperationSelection, OperationError> {\n                        let controller = controller_c;\n                        let Some(relative_root) = to.parent() else {\n                            return Err(OperationError::from_err(\n                                format!(\"path {} has no parent directory\", to.display()),\n                                &controller,\n                            ));\n                        };\n\n                        let op_sel = OperationSelection {\n                            ignored: paths.clone(),\n                            selected: vec![to.clone()],\n                        };\n\n                        let mut paths = paths;\n                        for path in &paths.clone() {\n                            if path.is_dir() {\n                                let new_paths_it = WalkDir::new(path).into_iter();\n                                for entry in new_paths_it.skip(1) {\n                                    let entry = entry\n                                        .map_err(|e| OperationError::from_err(e, &controller))?;\n                                    paths.push(entry.into_path());\n                                }\n                            }\n                        }\n\n                        match archive_type {\n                            ArchiveType::Tgz => {\n                                let mut archive = fs::File::create(&to)\n                                    .map(io::BufWriter::new)\n                                    .map(|w| {\n                                        flate2::write::GzEncoder::new(\n                                            w,\n                                            flate2::Compression::default(),\n                                        )\n                                    })\n                                    .map(tar::Builder::new)\n                                    .map_err(|e| OperationError::from_err(e, &controller))?;\n\n                                let total_paths = paths.len();\n                                for (i, path) in paths.iter().enumerate() {\n                                    futures::executor::block_on(async {\n                                        controller\n                                            .check()\n                                            .await\n                                            .map_err(|e| OperationError::from_state(e, &controller))\n                                    })?;\n\n                                    controller.set_progress((i as f32) / total_paths as f32);\n\n                                    if let Some(relative_path) = path\n                                        .strip_prefix(relative_root)\n                                        .map_err(|e| OperationError::from_err(e, &controller))?\n                                        .to_str()\n                                    {\n                                        archive\n                                            .append_path_with_name(path, relative_path)\n                                            .map_err(|e| {\n                                                OperationError::from_err(e, &controller)\n                                            })?;\n                                    }\n                                }\n\n                                archive\n                                    .finish()\n                                    .map_err(|e| OperationError::from_err(e, &controller))?;\n                            }\n                            ArchiveType::Zip => {\n                                let mut archive = fs::File::create(&to)\n                                    .map(io::BufWriter::new)\n                                    .map(zip::ZipWriter::new)\n                                    .map_err(|e| OperationError::from_err(e, &controller))?;\n\n                                let total_paths = paths.len();\n                                let mut buffer = vec![0; 4 * 1024 * 1024];\n                                for (i, path) in paths.iter().enumerate() {\n                                    futures::executor::block_on(async {\n                                        controller\n                                            .check()\n                                            .await\n                                            .map_err(|s| OperationError::from_state(s, &controller))\n                                    })?;\n\n                                    controller.set_progress((i as f32) / total_paths as f32);\n\n                                    let mut zip_options = zip::write::SimpleFileOptions::default();\n                                    if password.is_some() {\n                                        zip_options = zip_options.with_aes_encryption(\n                                            Aes256,\n                                            password.as_deref().unwrap(),\n                                        );\n                                    }\n                                    if let Some(relative_path) = path\n                                        .strip_prefix(relative_root)\n                                        .map_err(|e| OperationError::from_err(e, &controller))?\n                                        .to_str()\n                                    {\n                                        let mut file = fs::File::open(path).map_err(|e| {\n                                            OperationError::from_err(e, &controller)\n                                        })?;\n                                        let metadata = file.metadata().map_err(|e| {\n                                            OperationError::from_err(e, &controller)\n                                        })?;\n\n                                        if let Ok(modified) = metadata.modified()\n                                            && let Some(last_modified) =\n                                                archive::system_time_to_zip_date_time(modified)\n                                        {\n                                            zip_options =\n                                                zip_options.last_modified_time(last_modified);\n                                        }\n\n                                        #[cfg(unix)]\n                                        {\n                                            use std::os::unix::fs::MetadataExt;\n                                            let mode = metadata.mode();\n                                            zip_options = zip_options.unix_permissions(mode);\n                                        }\n\n                                        if path.is_file() {\n                                            let total = metadata.len();\n                                            if total >= 4 * 1024 * 1024 * 1024 {\n                                                // The large file option must be enabled for files above 4 GiB\n                                                zip_options = zip_options.large_file(true);\n                                            }\n                                            archive\n                                                .start_file(relative_path, zip_options)\n                                                .map_err(|e| {\n                                                    OperationError::from_err(e, &controller)\n                                                })?;\n                                            let mut current = 0;\n                                            loop {\n                                                futures::executor::block_on(async {\n                                                    controller.check().await.map_err(|s| {\n                                                        OperationError::from_state(s, &controller)\n                                                    })\n                                                })?;\n\n                                                let count =\n                                                    file.read(&mut buffer).map_err(|e| {\n                                                        OperationError::from_err(e, &controller)\n                                                    })?;\n                                                if count == 0 {\n                                                    break;\n                                                }\n                                                archive.write_all(&buffer[..count]).map_err(\n                                                    |e| OperationError::from_err(e, &controller),\n                                                )?;\n                                                current += count;\n\n                                                let file_progress = current as f32 / total as f32;\n                                                let total_progress =\n                                                    (i as f32 + file_progress) / total_paths as f32;\n                                                controller.set_progress(total_progress);\n                                            }\n                                        } else {\n                                            archive\n                                                .add_directory(relative_path, zip_options)\n                                                .map_err(|e| {\n                                                    OperationError::from_err(e, &controller)\n                                                })?;\n                                        }\n                                    }\n                                }\n\n                                archive\n                                    .finish()\n                                    .map_err(|e| OperationError::from_err(e, &controller))?;\n                            }\n                        }\n\n                        Ok(op_sel)\n                    },\n                )\n                .await\n                .map_err(wrap_compio_spawn_error)?\n            }\n            Self::Copy { paths, to } => {\n                copy_or_move(paths, to, Method::Copy, msg_tx, controller).await\n            }\n            Self::Delete { paths } => {\n                let total = paths.len();\n                for (i, path) in paths.into_iter().enumerate() {\n                    futures::executor::block_on(async {\n                        controller\n                            .check()\n                            .await\n                            .map_err(|s| OperationError::from_state(s, &controller))\n                    })?;\n\n                    controller.set_progress((i as f32) / (total as f32));\n\n                    let _items_opt = compio::runtime::spawn_blocking(|| trash::delete(path))\n                        .await\n                        .map_err(wrap_compio_spawn_error)?;\n                    //TODO: items_opt allows for easy restore\n                }\n                Ok(OperationSelection::default())\n            }\n            Self::DeleteTrash { items } => {\n                #[cfg(any(\n                    target_os = \"windows\",\n                    all(\n                        unix,\n                        not(target_os = \"macos\"),\n                        not(target_os = \"ios\"),\n                        not(target_os = \"android\")\n                    )\n                ))]\n                {\n                    let controller_clone = controller.clone();\n                    compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {\n                        let controller = controller_clone;\n                        let count = items.len();\n                        for (i, item) in items.into_iter().enumerate() {\n                            futures::executor::block_on(async {\n                                controller\n                                    .check()\n                                    .await\n                                    .map_err(|s| OperationError::from_state(s, &controller))\n                            })?;\n\n                            controller.set_progress(i as f32 / count as f32);\n\n                            trash::os_limited::purge_all([item])\n                                .map_err(|e| OperationError::from_err(e, &controller))?;\n                        }\n                        Ok(())\n                    })\n                    .await\n                    .map_err(wrap_compio_spawn_error)?\n                    .map_err(|e| OperationError::from_err(e, &controller))?;\n                }\n                Ok(OperationSelection::default())\n            }\n            Self::EmptyTrash => {\n                #[cfg(any(\n                    target_os = \"windows\",\n                    all(\n                        unix,\n                        not(target_os = \"macos\"),\n                        not(target_os = \"ios\"),\n                        not(target_os = \"android\")\n                    )\n                ))]\n                {\n                    let controller_clone = controller.clone();\n                    compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {\n                        let controller = controller_clone;\n                        let items = trash::os_limited::list()\n                            .map_err(|e| OperationError::from_err(e, &controller))?;\n                        let count = items.len();\n                        let mut errors: Vec<trash::Error> = Vec::new();\n\n                        for (i, item) in items.into_iter().enumerate() {\n                            futures::executor::block_on(async {\n                                controller\n                                    .check()\n                                    .await\n                                    .map_err(|s| OperationError::from_state(s, &controller))\n                            })?;\n\n                            if let Err(e) = trash::os_limited::purge_all([item]) {\n                                errors.push(e);\n                            }\n\n                            controller.set_progress(i as f32 / count as f32);\n                        }\n\n                        // Report errors at the end\n                        if !errors.is_empty() {\n                            log::warn!(\"Failed to purge {} items:\", errors.len());\n                            for e in &errors {\n                                log::warn!(\"  - {e}\");\n                            }\n\n                            // Return an error to signal partial failure\n                            return Err(OperationError::from_err(\n                                format!(\n                                    \"Failed to delete {} of {} items. Check log for details.\",\n                                    errors.len(),\n                                    count\n                                ),\n                                &controller,\n                            ));\n                        }\n\n                        Ok(())\n                    })\n                    .await\n                    .map_err(wrap_compio_spawn_error)?\n                    .map_err(|e| OperationError::from_err(e, &controller))?;\n                }\n                Ok(OperationSelection::default())\n            }\n            Self::Extract {\n                paths,\n                to,\n                password,\n            } => {\n                let controller_clone = controller.clone();\n                compio::runtime::spawn_blocking(\n                    move || -> Result<OperationSelection, OperationError> {\n                        let controller = controller_clone;\n                        let total_paths = paths.len();\n                        let mut op_sel = OperationSelection::default();\n                        for (i, path) in paths.iter().enumerate() {\n                            futures::executor::block_on(async {\n                                controller\n                                    .check()\n                                    .await\n                                    .map_err(|s| OperationError::from_state(s, &controller))\n                            })?;\n\n                            controller.set_progress((i as f32) / total_paths as f32);\n\n                            if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {\n                                let dir_name = get_directory_name(file_name);\n                                let mut new_dir = to.join(dir_name);\n\n                                if new_dir.exists()\n                                    && let Some(new_dir_parent) = new_dir.parent()\n                                {\n                                    new_dir = copy_unique_path(&new_dir, new_dir_parent);\n                                }\n\n                                op_sel.ignored.push(path.clone());\n                                op_sel.selected.push(new_dir.clone());\n\n                                crate::archive::extract(path, &new_dir, &password, &controller)?;\n                            }\n                        }\n\n                        Ok(op_sel)\n                    },\n                )\n            }\n            .await\n            .map_err(wrap_compio_spawn_error)?,\n            Self::Move {\n                paths,\n                to,\n                cross_device_copy,\n            } => {\n                copy_or_move(\n                    paths,\n                    to,\n                    Method::Move { cross_device_copy },\n                    msg_tx,\n                    controller,\n                )\n                .await\n            }\n            Self::NewFolder { path } => {\n                let controller_clone = controller.clone();\n                compio::runtime::spawn(async move {\n                    let controller = controller_clone;\n                    controller\n                        .check()\n                        .await\n                        .map_err(|s| OperationError::from_state(s, &controller))?;\n                    compio::fs::create_dir(&path)\n                        .await\n                        .map_err(|e| OperationError::from_err(e, &controller))?;\n                    Result::<_, OperationError>::Ok(OperationSelection {\n                        ignored: Vec::new(),\n                        selected: vec![path],\n                    })\n                })\n            }\n            .await\n            .map_err(wrap_compio_spawn_error)?,\n            Self::NewFile { path } => {\n                let controller_clone = controller.clone();\n                compio::runtime::spawn(async move {\n                    let controller = controller_clone;\n                    controller\n                        .check()\n                        .await\n                        .map_err(|s| OperationError::from_state(s, &controller))?;\n                    compio::fs::File::create(&path)\n                        .await\n                        .map_err(|e| OperationError::from_err(e, &controller))?;\n                    Result::<_, OperationError>::Ok(OperationSelection {\n                        ignored: Vec::new(),\n                        selected: vec![path],\n                    })\n                })\n            }\n            .await\n            .map_err(wrap_compio_spawn_error)?,\n            Self::PermanentlyDelete { paths } => {\n                let total = paths.len();\n                for (idx, path) in paths.into_iter().enumerate() {\n                    controller\n                        .check()\n                        .await\n                        .map_err(|s| OperationError::from_state(s, &controller))?;\n\n                    controller.set_progress((idx as f32) / (total as f32));\n\n                    tokio::task::spawn_blocking(|| {\n                        if path.is_symlink() || path.is_file() {\n                            fs::remove_file(path)\n                        } else if path.is_dir() {\n                            fs::remove_dir_all(path)\n                        } else {\n                            Err(std::io::Error::new(\n                                std::io::ErrorKind::InvalidData,\n                                \"File to delete is not symlink, file or directory\",\n                            ))\n                        }\n                    })\n                    .await\n                    .map_err(|e| OperationError::from_err(e, &controller))?\n                    .map_err(|e| OperationError::from_err(e, &controller))?;\n                }\n\n                Ok(OperationSelection::default())\n            }\n            Self::RemoveFromRecents { paths } => {\n                tokio::task::spawn_blocking(move || {\n                    let path_refs = paths.iter().map(PathBuf::as_path).collect::<Box<[_]>>();\n                    recently_used_xbel::remove_recently_used(&path_refs)\n                })\n                .await\n                .map_err(|e| OperationError::from_err(e, &controller))?\n                .map_err(|e| OperationError::from_err(e, &controller))?;\n\n                Ok(OperationSelection::default())\n            }\n            Self::Rename { from, to } => {\n                let controller_clone = controller.clone();\n\n                compio::runtime::spawn(async move {\n                    let controller = controller_clone;\n                    controller\n                        .check()\n                        .await\n                        .map_err(|s| OperationError::from_state(s, &controller))?;\n                    compio::fs::rename(&from, &to)\n                        .await\n                        .map_err(|e| OperationError::from_err(e, &controller))?;\n                    Result::<_, OperationError>::Ok(OperationSelection {\n                        ignored: vec![from],\n                        selected: vec![to],\n                    })\n                })\n            }\n            .await\n            .map_err(wrap_compio_spawn_error)?,\n            #[cfg(target_os = \"macos\")]\n            Self::Restore { .. } => {\n                // TODO: add support for macos\n                return Err(OperationError::from_msg(\n                    \"Restoring from trash is not supported on macos\",\n                ));\n            }\n            #[cfg(not(target_os = \"macos\"))]\n            Self::Restore { items } => {\n                let total = items.len();\n                let mut paths = Vec::with_capacity(total);\n                for (i, item) in items.into_iter().enumerate() {\n                    controller\n                        .check()\n                        .await\n                        .map_err(|s| OperationError::from_state(s, &controller))?;\n\n                    controller.set_progress((i as f32) / (total as f32));\n\n                    paths.push(item.original_path());\n\n                    compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item]))\n                        .await\n                        .map_err(wrap_compio_spawn_error)?\n                        .map_err(|e| OperationError::from_err(e, &controller))?;\n                }\n                Ok(OperationSelection {\n                    ignored: Vec::new(),\n                    selected: paths,\n                })\n            }\n            Self::SetExecutableAndLaunch { path } => {\n                controller\n                    .check()\n                    .await\n                    .map_err(|s| OperationError::from_state(s, &controller))?;\n\n                let controller_clone = controller.clone();\n                compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {\n                    let controller = controller_clone;\n                    //TODO: what to do on non-Unix systems?\n                    #[cfg(unix)]\n                    {\n                        use std::os::unix::fs::PermissionsExt;\n\n                        let mut perms = fs::metadata(&path)\n                            .map_err(|e| OperationError::from_err(e, &controller))?\n                            .permissions();\n                        let current_mode = perms.mode();\n                        let new_mode = current_mode | 0o111;\n                        perms.set_mode(new_mode);\n                        fs::set_permissions(&path, perms)\n                            .map_err(|e| OperationError::from_err(e, &controller))?;\n                    }\n\n                    let mut command = std::process::Command::new(path);\n                    spawn_detached(&mut command)\n                        .map_err(|e| OperationError::from_err(e, &controller))?;\n\n                    Ok(())\n                })\n                .await\n                .map_err(wrap_compio_spawn_error)?\n                .map_err(|e| OperationError::from_err(e, &controller))?;\n                Ok(OperationSelection::default())\n            }\n            Self::SetPermissions { path, mode } => {\n                controller\n                    .check()\n                    .await\n                    .map_err(|s| OperationError::from_state(s, &controller))?;\n\n                let controller_clone = controller.clone();\n                let path_clone = path.clone();\n                compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {\n                    let controller = controller_clone;\n                    let path = path_clone;\n                    //TODO: what to do on non-Unix systems?\n                    #[cfg(unix)]\n                    {\n                        use std::os::unix::fs::PermissionsExt;\n                        let perms = fs::Permissions::from_mode(mode);\n                        fs::set_permissions(&path, perms)\n                            .map_err(|e| OperationError::from_err(e, &controller))?;\n                    }\n\n                    Ok(())\n                })\n                .await\n                .map_err(wrap_compio_spawn_error)?\n                .map_err(|e| OperationError::from_err(e, &controller))?;\n                Ok(OperationSelection {\n                    ignored: Vec::new(),\n                    selected: vec![path],\n                })\n            }\n        };\n\n        controller_clone.set_progress(1.0);\n\n        paths\n    }\n}\n\n#[track_caller]\nfn wrap_compio_spawn_error(err: Box<dyn std::any::Any + Send>) -> OperationError {\n    log::error!(\n        \"compio runtime spawn failed: {}\",\n        std::backtrace::Backtrace::capture()\n    );\n\n    // Preserve error if it's already an OperationError\n    if let Ok(err) = err.downcast() {\n        *err\n    } else {\n        OperationError::from_msg(\"compio runtime spawn failed\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::fs::{self, File};\n    use std::io;\n    use std::path::PathBuf;\n\n    use cosmic::iced::futures::channel::mpsc;\n    use cosmic::iced::futures::{StreamExt, future};\n    use log::debug;\n    use test_log::test;\n    use tokio::sync;\n\n    use super::{Controller, Operation, OperationError, OperationSelection, ReplaceResult};\n    use crate::app::test_utils::{\n        NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs, filter_files,\n        simple_fs,\n    };\n    use crate::app::{DialogPage, Message};\n    use crate::fl;\n\n    /// Simple wrapper around `[Operation::Copy]`\n    pub async fn operation_copy(\n        paths: Vec<PathBuf>,\n        to: PathBuf,\n    ) -> Result<OperationSelection, OperationError> {\n        let id = fastrand::u64(0..u64::MAX);\n        let (tx, mut rx) = mpsc::channel(1);\n        let paths_clone = paths.clone();\n        let to_clone = to.clone();\n\n        // Wrap this into its own future so that it may be polled concurerntly with the message handler.\n        let handle_copy = async move {\n            Operation::Copy {\n                paths: paths_clone,\n                to: to_clone,\n            }\n            .perform(&sync::Mutex::new(tx).into(), Controller::default())\n            .await\n        };\n\n        // Concurrently handling messages will prevent the mpsc channel from blocking when full.\n        let handle_messages = async move {\n            while let Some(msg) = rx.next().await {\n                match msg {\n                    Message::DialogPush(DialogPage::Replace { tx, .. }, _id_to_focus) => {\n                        debug!(\"[{id}] Replace request\");\n                        tx.send(ReplaceResult::Cancel)\n                            .await\n                            .expect(\"Sending a response to a replace request should succeed\");\n                    }\n                    _ => unreachable!(\n                        \"Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation\"\n                    ),\n                }\n            }\n        };\n\n        future::join(handle_messages, handle_copy).await.1\n    }\n\n    #[test(compio::test)]\n    async fn copy_file_to_same_location() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, 0, 1, 0, NAME_LEN)?;\n        let path = fs.path();\n\n        // Get the first file from the first directory\n        let first_dir = filter_dirs(path)?\n            .next()\n            .expect(\"Should have at least one directory\");\n        let first_file = filter_files(&first_dir)?\n            .next()\n            .expect(\"Should have at least one file\");\n\n        // Duplicate that file\n        let base_name = first_file\n            .file_name()\n            .and_then(|name| name.to_str())\n            .expect(\"File name exists and is valid\");\n        debug!(\n            \"Duplicating {} in {}\",\n            first_file.display(),\n            first_dir.display()\n        );\n        operation_copy(vec![first_file.clone()], first_dir.clone())\n            .await\n            .expect(\"Copy operation should have succeeded\");\n\n        assert!(first_file.exists(), \"Original file should still exist\");\n        let expected = first_dir.join(format!(\"{base_name} ({} 1)\", fl!(\"copy_noun\")));\n        assert!(expected.exists(), \"File should have been duplicated\");\n\n        Ok(())\n    }\n\n    #[test(compio::test)]\n    async fn copy_file_with_extension_to_same_loc() -> io::Result<()> {\n        let fs = empty_fs()?;\n        let path = fs.path();\n\n        let base_name = \"foo.txt\";\n        let base_path = path.join(base_name);\n        File::create(&base_path)?;\n        debug!(\"Duplicating {}\", base_path.display());\n        operation_copy(vec![base_path.clone()], path.to_owned())\n            .await\n            .expect(\"Copy operation should have succeeded\");\n\n        assert!(base_path.exists(), \"Original file should still exist\");\n        let expected = path.join(format!(\"foo ({} 1).txt\", fl!(\"copy_noun\")));\n        assert!(expected.exists(), \"File should have been duplicated\");\n\n        Ok(())\n    }\n\n    #[test(compio::test)]\n    async fn copy_dir_to_same_location() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, 0, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        // First directory path\n        let first_dir = filter_dirs(path)?\n            .next()\n            .expect(\"Should have at least one directory\");\n        let base_name = first_dir\n            .file_name()\n            .and_then(|name| name.to_str())\n            .expect(\"First directory exists and has a valid name\");\n        debug!(\"Duplicating directory {}\", first_dir.display());\n        operation_copy(vec![first_dir.clone()], path.to_owned())\n            .await\n            .expect(\"Copy operation should have succeeded\");\n\n        assert!(first_dir.exists(), \"Original directory should still exist\");\n        let expected = path.join(format!(\"{base_name} ({} 1)\", fl!(\"copy_noun\")));\n        assert!(expected.exists(), \"Directory should have been duplicated\");\n\n        Ok(())\n    }\n\n    #[test(compio::test)]\n    async fn copying_file_multiple_times_to_same_location() -> io::Result<()> {\n        let fs = empty_fs()?;\n        let path = fs.path();\n\n        let base_name = \"cosmic\";\n        let base_path = path.join(base_name);\n        File::create(&base_path)?;\n\n        for i in 1..5 {\n            debug!(\"Duplicating {}\", base_path.display());\n            operation_copy(vec![base_path.clone()], path.to_owned())\n                .await\n                .expect(\"Copy operation should have succeeded\");\n            assert!(base_path.exists(), \"Original file should still exist\");\n            assert!(\n                path.join(format!(\"{base_name} ({} {i})\", fl!(\"copy_noun\")))\n                    .exists(),\n                \"File should have been duplicated (copy #{i})\"\n            );\n        }\n\n        Ok(())\n    }\n\n    #[test(compio::test)]\n    async fn copy_to_diff_dir_doesnt_dupe_files() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        let (first_dir, second_dir) = {\n            let mut dirs = filter_dirs(path)?;\n            (\n                dirs.next().expect(\"Should have at least two dirs\"),\n                dirs.next().expect(\"Should have at least two dirs\"),\n            )\n        };\n        let first_file = filter_files(&first_dir)?\n            .next()\n            .expect(\"Should have at least one file\");\n        // Both directories have a file with the same name.\n        let base_name = first_file\n            .file_name()\n            .and_then(|name| name.to_str())\n            .expect(\"File name exists and is valid\");\n\n        debug!(\n            \"Copying {} to {}\",\n            first_file.display(),\n            second_dir.display()\n        );\n        operation_copy(vec![first_file.clone()], second_dir.clone())\n            .await\n            .expect(concat!(\n                \"Copy operation should have been cancelled \",\n                \"because we're copying to different directories \",\n                \"without replacement\"\n            ));\n        assert!(\n            first_dir.join(base_name).exists(),\n            \"First file should still exist\"\n        );\n        assert!(\n            second_dir.join(base_name).exists(),\n            \"Second file should still exist\"\n        );\n\n        Ok(())\n    }\n\n    #[test(compio::test)]\n    async fn copy_file_with_diff_name_to_diff_dir() -> io::Result<()> {\n        let fs = empty_fs()?;\n        let path = fs.path();\n\n        let dir_path = path.join(\"cosmic\");\n        fs::create_dir(&dir_path)?;\n        let file_path = path.join(\"ferris\");\n        File::create(&file_path)?;\n        let expected = dir_path.join(\"ferris\");\n\n        debug!(\"Copying {} to {}\", file_path.display(), expected.display());\n        operation_copy(vec![file_path.clone()], dir_path.clone())\n            .await\n            .expect(\"Copy operation should have succeeded\");\n\n        assert!(file_path.exists(), \"Original file should still exist\");\n        assert!(expected.exists(), \"File should have been copied\");\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/operation/notifiers.rs",
    "content": "// Copyright 2026 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse std::path::{Path, PathBuf};\nuse std::sync::{Arc, LazyLock, Mutex};\nuse tokio::sync::Notify;\n\n/// Monitor files which are being written to.\npub struct FileWritingNotifier {\n    data: Vec<PathBuf>,\n    notify: Arc<Notify>,\n}\n\nstatic ACTIVELY_WRITING: LazyLock<Mutex<FileWritingNotifier>> = LazyLock::new(|| {\n    Mutex::new(FileWritingNotifier {\n        data: Vec::new(),\n        notify: Arc::new(Notify::new()),\n    })\n});\n\n/// Append path that is being written to.\npub fn actively_writing_add(path: PathBuf) {\n    ACTIVELY_WRITING.lock().unwrap().data.push(path);\n}\n\n/// Remove path to file that has finished writing and notify waiters.\npub fn actively_writing_remove(path: &Path) {\n    let mut guard = ACTIVELY_WRITING.lock().unwrap();\n    guard.data.retain(|p| p != path);\n    guard.notify.notify_waiters();\n}\n\n/// Wait until the actively-writing queue is empty or a file has been removed.\npub async fn actively_writing_tick() {\n    let notify = (|| {\n        let guard = ACTIVELY_WRITING.lock().unwrap();\n\n        if !guard.data.is_empty() {\n            return Some(guard.notify.clone());\n        }\n\n        None\n    })();\n\n    if let Some(notify) = notify {\n        notify.notified().await\n    }\n}\n\n/// Check if a file is being written to. Avoid thumbnail generation until after it is finished.\npub fn is_actively_writing_to(path: &Path) -> bool {\n    ACTIVELY_WRITING\n        .lock()\n        .unwrap()\n        .data\n        .iter()\n        .any(|p| p == path)\n}\n"
  },
  {
    "path": "src/operation/reader.rs",
    "content": "use std::path::Path;\nuse std::{fs, io};\n\nuse crate::operation::OperationError;\n\nuse super::Controller;\n\n// Special reader just for operations, handling cancel and progress\npub struct OpReader {\n    file: fs::File,\n    metadata: fs::Metadata,\n    current: u64,\n    controller: Controller,\n}\n\nimpl OpReader {\n    pub fn new<P: AsRef<Path>>(path: P, controller: Controller) -> io::Result<Self> {\n        let file = fs::File::open(&path)?;\n        let metadata = file.metadata()?;\n        Ok(Self {\n            file,\n            metadata,\n            current: 0,\n            controller,\n        })\n    }\n}\n\nimpl io::Read for OpReader {\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n        cosmic::iced::futures::executor::block_on(async {\n            self.controller\n                .check()\n                .await\n                .map_err(|s| io::Error::other(OperationError::from_state(s, &self.controller)))\n        })?;\n\n        let count = self.file.read(buf)?;\n        self.current += count as u64;\n\n        let progress = self.current as f32 / self.metadata.len() as f32;\n        self.controller.set_progress(progress);\n\n        Ok(count)\n    }\n}\n"
  },
  {
    "path": "src/operation/recursive.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\nuse super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};\nuse crate::operation::{OperationError, sync_to_disk};\nuse anyhow::Context as AnyhowContext;\nuse compio::BufResult;\nuse compio::buf::{IntoInner, IoBuf};\nuse compio::driver::ToSharedFd;\nuse compio::driver::op::AsyncifyFd;\nuse compio::io::{AsyncReadAt, AsyncWriteAt};\nuse cosmic::iced::futures;\nuse futures::{FutureExt, StreamExt};\nuse std::cell::Cell;\nuse std::error::Error;\nuse std::fs;\nuse std::future::Future;\nuse std::ops::ControlFlow;\nuse std::path::PathBuf;\nuse std::pin::Pin;\nuse std::rc::Rc;\nuse std::time::Instant;\nuse walkdir::WalkDir;\n\n#[cfg(feature = \"gvfs\")]\nuse gio::prelude::FileExtManual;\n\n#[derive(thiserror::Error, Debug)]\npub enum GioCopyError {\n    #[error(\"controller state\")]\n    Controller(OperationError),\n    #[cfg(feature = \"gvfs\")]\n    #[error(\"gio copy failed\")]\n    GLib(#[from] glib::Error),\n}\n\npub enum Method {\n    Copy,\n    Move { cross_device_copy: bool },\n}\n\npub struct Context {\n    buf: Vec<u8>,\n    controller: Controller,\n    on_progress: Box<dyn OnProgress>,\n    on_replace: Pin<Box<dyn OnReplace>>,\n    pub(crate) op_sel: OperationSelection,\n    replace_result_opt: Option<ReplaceResult>,\n    remaining_conflicts: usize,\n}\n\npub trait OnProgress: Fn(&Op, &Progress) + 'static {}\nimpl<F> OnProgress for F where F: Fn(&Op, &Progress) + 'static {}\n\npub trait OnReplace:\n    for<'a> Fn(&'a Op, usize) -> Pin<Box<dyn Future<Output = ReplaceResult> + 'a>> + 'static\n{\n}\nimpl<F> OnReplace for F where\n    F: for<'a> Fn(&'a Op, usize) -> Pin<Box<dyn Future<Output = ReplaceResult> + 'a>> + 'static\n{\n}\n\nimpl Context {\n    pub fn new(controller: Controller) -> Self {\n        Self {\n            // 128K is the optimal upper size of a buffer.\n            buf: vec![0u8; 128 * 1024],\n            controller,\n            on_progress: Box::new(|_op, _progress| {}),\n            on_replace: Box::pin(|_op, _count| Box::pin(async { ReplaceResult::Cancel })),\n            op_sel: OperationSelection::default(),\n            replace_result_opt: None,\n            remaining_conflicts: 0,\n        }\n    }\n\n    pub async fn recursive_copy_or_move(\n        &mut self,\n        from_to_pairs: impl IntoIterator<Item = (PathBuf, PathBuf)>,\n        method: Method,\n    ) -> Result<bool, OperationError> {\n        let mut ops = Vec::new();\n        let mut cleanup_ops = Vec::new();\n        let mut written_files = Vec::new();\n        let mut target_dirs = std::collections::HashSet::new();\n        for (from_parent, to_parent) in from_to_pairs {\n            self.controller\n                .check()\n                .await\n                .map_err(|s| OperationError::from_state(s, &self.controller))?;\n\n            if from_parent == to_parent {\n                // Skip matching source and destination\n                continue;\n            }\n\n            for entry in WalkDir::new(&from_parent) {\n                self.controller\n                    .check()\n                    .await\n                    .map_err(|s| OperationError::from_state(s, &self.controller))?;\n\n                let entry = entry.map_err(|err| {\n                    OperationError::from_err(\n                        format!(\n                            \"failed to walk directory {}: {}\",\n                            from_parent.display(),\n                            err\n                        ),\n                        &self.controller,\n                    )\n                })?;\n                let file_type = entry.file_type();\n                let from = entry.into_path();\n                let kind = if file_type.is_dir() {\n                    OpKind::Mkdir\n                } else if file_type.is_file() {\n                    match method {\n                        Method::Copy => OpKind::Copy,\n                        Method::Move { cross_device_copy } => OpKind::Move { cross_device_copy },\n                    }\n                } else if file_type.is_symlink() {\n                    let target = fs::read_link(&from).map_err(|err| {\n                        OperationError::from_err(\n                            format!(\"failed to read link {}: {}\", from_parent.display(), err),\n                            &self.controller,\n                        )\n                    })?;\n                    OpKind::Symlink { target }\n                } else {\n                    //TODO: present dialog and allow continue\n                    return Err(OperationError::from_err(\n                        format!(\"{} is not a known file type\", from.display()),\n                        &self.controller,\n                    ));\n                };\n                let to = if from == from_parent {\n                    // When copying a file, from matches from_parent, and to_parent must be used\n                    to_parent.clone()\n                } else {\n                    let relative = from.strip_prefix(&from_parent).map_err(|err| {\n                        OperationError::from_err(\n                            format!(\n                                \"failed to remove prefix {} from {}: {}\",\n                                from_parent.display(),\n                                from.display(),\n                                err\n                            ),\n                            &self.controller,\n                        )\n                    })?;\n                    //TODO: ensure to is inside of to_parent?\n                    to_parent.join(relative)\n                };\n                let op = Op {\n                    kind,\n                    from,\n                    to,\n                    skipped: Rc::new(Skip {\n                        normal: Cell::new(false),\n                        cleanup: Cell::new(false),\n                    }),\n                    is_cleanup: false,\n                };\n                if matches!(method, Method::Move { .. })\n                    && let Some(cleanup_op) = op.move_cleanup_op()\n                {\n                    cleanup_ops.push(cleanup_op);\n                }\n                if let Some(parent) = op.to.parent() {\n                    target_dirs.insert(parent.to_path_buf());\n                }\n                ops.push(op);\n            }\n\n            self.op_sel.ignored.push(from_parent);\n        }\n\n        // Add cleanup ops after standard ops, in reverse\n        cleanup_ops.reverse();\n        ops.append(&mut cleanup_ops);\n\n        // Count potential conflicts (files that would need replacement)\n        self.remaining_conflicts = ops\n            .iter()\n            .filter(|op| {\n                matches!(\n                    op.kind,\n                    OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. }\n                ) && op.to.is_file()\n            })\n            .count();\n\n        let total_ops = ops.len();\n        for (current_ops, mut op) in ops.into_iter().enumerate() {\n            self.controller\n                .check()\n                .await\n                .map_err(|s| OperationError::from_state(s, &self.controller))?;\n\n            let progress = Progress {\n                current_ops,\n                total_ops,\n                current_bytes: 0,\n                total_bytes: None,\n            };\n            (self.on_progress)(&op, &progress);\n            if op.run(self, progress).await.map_err(|err| {\n                OperationError::from_err(\n                    format!(\n                        \"failed to {:?} {} to {}: {}\",\n                        op.kind,\n                        op.from.display(),\n                        op.to.display(),\n                        err\n                    ),\n                    &self.controller,\n                )\n            })? {\n                if matches!(\n                    op.kind,\n                    OpKind::Copy\n                        | OpKind::Move {\n                            cross_device_copy: true\n                        }\n                ) {\n                    written_files.push(op.to.clone());\n                }\n                // The from path is ignored in the operation selection if it is a top level item\n                if self.op_sel.ignored.contains(&op.from) {\n                    // So add the to path to the selection\n                    self.op_sel.selected.push(op.to);\n                }\n            } else {\n                // Cancelled\n                return Ok(false);\n            }\n        }\n\n        // Flush files to disk\n        sync_to_disk(written_files, target_dirs).await;\n\n        Ok(true)\n    }\n\n    pub fn on_progress<F: OnProgress>(mut self, f: F) -> Self {\n        self.on_progress = Box::new(f);\n        self\n    }\n\n    pub fn on_replace(mut self, f: impl OnReplace + 'static) -> Self {\n        self.on_replace = Box::pin(f);\n        self\n    }\n\n    async fn replace(&mut self, op: &Op) -> Result<ControlFlow<bool, PathBuf>, Box<dyn Error>> {\n        let replace_result = match self.replace_result_opt {\n            Some(result) => result,\n            None => (self.on_replace)(op, self.remaining_conflicts).await,\n        };\n\n        match replace_result {\n            ReplaceResult::Replace(apply_to_all) => {\n                if apply_to_all {\n                    self.replace_result_opt = Some(replace_result);\n                }\n                compio::fs::remove_file(&op.to).await?;\n                Ok(ControlFlow::Continue(op.to.clone()))\n            }\n            ReplaceResult::KeepBoth => match op.to.parent() {\n                Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),\n                None => Err(format!(\"failed to get parent of {}\", op.to.display()).into()),\n            },\n            ReplaceResult::Skip(apply_to_all) => {\n                if apply_to_all {\n                    self.replace_result_opt = Some(replace_result);\n                }\n                op.skipped.normal.set(true);\n                Ok(ControlFlow::Break(true))\n            }\n            ReplaceResult::Cancel => Ok(ControlFlow::Break(false)),\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct Progress {\n    pub current_ops: usize,\n    pub total_ops: usize,\n    pub current_bytes: u64,\n    pub total_bytes: Option<u64>,\n}\n\n#[derive(Debug)]\npub enum OpKind {\n    Copy,\n    Move { cross_device_copy: bool },\n    Mkdir,\n    Remove,\n    Rmdir,\n    Symlink { target: PathBuf },\n}\n\n#[derive(Debug)]\npub struct Skip {\n    /// Normal operation should be skipped\n    pub normal: Cell<bool>,\n    /// Cleanup operation should be skipped\n    pub cleanup: Cell<bool>,\n}\n\n#[derive(Debug)]\npub struct Op {\n    pub kind: OpKind,\n    pub from: PathBuf,\n    pub to: PathBuf,\n    pub skipped: Rc<Skip>,\n    pub is_cleanup: bool,\n}\n\nimpl Op {\n    fn move_cleanup_op(&self) -> Option<Self> {\n        let kind = match self.kind {\n            OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. } => OpKind::Remove,\n            OpKind::Mkdir => OpKind::Rmdir,\n            OpKind::Remove | OpKind::Rmdir => return None,\n        };\n        Some(Self {\n            kind,\n            from: self.from.clone(),\n            //TODO: it is strange to have `to` here\n            to: self.to.clone(),\n            skipped: self.skipped.clone(),\n            is_cleanup: true,\n        })\n    }\n\n    async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> {\n        if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {\n            return Ok(true);\n        }\n        match self.kind {\n            OpKind::Copy => {\n                crate::operation::actively_writing_add(self.to.clone());\n                let result = self.copy(ctx, progress).await;\n\n                if result.is_err() {\n                    _ = compio::fs::remove_file(&self.to).await;\n                }\n\n                crate::operation::actively_writing_remove(&self.to);\n                return result;\n            }\n            OpKind::Move { cross_device_copy } => {\n                // Do not clean up if cross_device_copy is set\n                if cross_device_copy {\n                    self.skipped.cleanup.set(true);\n                }\n\n                // Remove `to` if overwriting and it is an existing file\n                if self.to.is_file() {\n                    match ctx.replace(self).await? {\n                        ControlFlow::Continue(to) => {\n                            self.to = to;\n                        }\n                        ControlFlow::Break(ret) => {\n                            return Ok(ret);\n                        }\n                    }\n                }\n                // This is atomic and ensures `to` is not created by any other process\n                match compio::fs::hard_link(&self.from, &self.to).await {\n                    Ok(()) => {}\n                    Err(err) => {\n                        // https://docs.rs/windows-sys/latest/windows_sys/Win32/Foundation/constant.ERROR_NOT_SAME_DEVICE.html\n                        #[cfg(windows)]\n                        const EXDEV: i32 = 17;\n                        #[cfg(unix)]\n                        const EXDEV: i32 = libc::EXDEV as _;\n\n                        if err.raw_os_error() == Some(EXDEV) {\n                            if cross_device_copy {\n                                // Do not clean up if cross_device_copy is set\n                                self.skipped.cleanup.set(true);\n                            }\n                            // Try standard copy if hard link fails with cross device error\n                            let mut copy_op = Self {\n                                kind: OpKind::Copy,\n                                from: self.from.clone(),\n                                to: self.to.clone(),\n                                skipped: self.skipped.clone(),\n                                is_cleanup: self.is_cleanup,\n                            };\n                            return Box::pin(copy_op.run(ctx, progress)).await;\n                        }\n                        return Err(err.into());\n                    }\n                }\n            }\n            OpKind::Mkdir => {\n                compio::fs::create_dir_all(&self.to).await?;\n            }\n            OpKind::Remove => {\n                compio::fs::remove_file(&self.from).await?;\n            }\n            OpKind::Rmdir => {\n                compio::fs::remove_dir(&self.from).await?;\n            }\n            OpKind::Symlink { ref target } => {\n                // Remove `to` if overwriting and it is an existing file\n                if self.to.is_file() {\n                    match ctx.replace(self).await? {\n                        ControlFlow::Continue(to) => {\n                            self.to = to;\n                        }\n                        ControlFlow::Break(ret) => {\n                            return Ok(ret);\n                        }\n                    }\n                }\n                #[cfg(unix)]\n                {\n                    std::os::unix::fs::symlink(target, &self.to)?;\n                }\n                #[cfg(windows)]\n                {\n                    if target.is_dir() {\n                        std::os::windows::fs::symlink_dir(target, &self.to)?;\n                    } else {\n                        std::os::windows::fs::symlink_file(target, &self.to)?;\n                    }\n                }\n            }\n        }\n        Ok(true)\n    }\n\n    async fn copy(\n        &mut self,\n        ctx: &mut Context,\n        mut progress: Progress,\n    ) -> Result<bool, Box<dyn Error>> {\n        // Remove `to` if overwriting and it is an existing file\n        if self.to.is_file() {\n            match ctx.replace(self).await? {\n                ControlFlow::Continue(to) => {\n                    self.to = to;\n                }\n                ControlFlow::Break(ret) => {\n                    return Ok(ret);\n                }\n            }\n        }\n\n        let (from_file, metadata, to_file) = cosmic::iced::futures::join!(\n            async {\n                compio::fs::OpenOptions::new()\n                    .read(true)\n                    .open(&self.from)\n                    .await\n                    .with_context(|| format!(\"failed to open {} for reading\", self.from.display(),))\n            },\n            async { compio::fs::metadata(&self.from).await.ok() },\n            // This is atomic and ensures `to` is not created by any other process\n            async {\n                compio::fs::OpenOptions::new()\n                    .create_new(true)\n                    .write(true)\n                    .open(&self.to)\n                    .await\n                    .with_context(|| format!(\"failed to open {} for writing\", self.to.display()))\n            }\n        );\n\n        let from_file = from_file?;\n        let mut to_file = to_file?;\n        progress.total_bytes = metadata.as_ref().map(|m| m.len());\n        (ctx.on_progress)(self, &progress);\n\n        if let Some(metadata) = metadata.as_ref()\n            && let Err(why) = to_file.set_permissions(metadata.permissions()).await\n        {\n            // This error is not propagated upwards as some filesystems do not support setting permissions\n            if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {\n                tracing::warn!(?why, \"failed to set permissions for {}\", self.to.display(),);\n            }\n        }\n\n        // Prevent spamming the progress callbacks.\n        let mut last_progress_update = Instant::now();\n        // io_uring/IOCP requires transferring ownership of the buffer to the kernel.\n        let mut buf_in = std::mem::take(&mut ctx.buf);\n        // Track where the current read/write position is at.\n        let mut pos = 0;\n\n        loop {\n            let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;\n\n            let count = match result {\n                Ok(0) => {\n                    buf_in = buf_out;\n                    break;\n                }\n                Ok(count) => count,\n                Err(why) => {\n                    ctx.buf = buf_out;\n                    tracing::error!(\"failed to read: {:?}\", why);\n                    _ = futures::future::join(from_file.close(), to_file.close()).await;\n                    return Err(why).context(\"failed to read\")?;\n                }\n            };\n\n            let BufResult(result, buf_out_slice) =\n                to_file.write_at(buf_out.slice(..count), pos).await;\n            let buf_out = buf_out_slice.into_inner();\n\n            if let Err(why) = result {\n                #[cfg(feature = \"gvfs\")]\n                if let std::io::ErrorKind::Unsupported = why.kind() {\n                    ctx.buf = buf_out;\n                    _ = futures::future::join(from_file.close(), to_file.close()).await;\n                    return self\n                        .gio_file_copy(ctx, progress)\n                        .await\n                        .map(|_| true)\n                        .map_err(Into::into);\n                }\n\n                tracing::error!(\"failed to write: {:?}\", why);\n                ctx.buf = buf_out;\n                _ = futures::future::join(from_file.close(), to_file.close()).await;\n                return Err(why).context(\"failed to write\")?;\n            }\n\n            progress.current_bytes += count as u64;\n            pos += count as u64;\n\n            // Avoid spamming progress messages too early.\n            let current = Instant::now();\n            if current.duration_since(last_progress_update).as_millis() > 49 {\n                last_progress_update = current;\n                (ctx.on_progress)(self, &progress);\n\n                // Also check if the progress was cancelled.\n                if let Err(state) = ctx.controller.check().await {\n                    ctx.buf = buf_out;\n                    tracing::warn!(\n                        \"operation to copy from {:?} to {:?} cancelled\",\n                        self.from,\n                        self.to\n                    );\n                    _ = futures::future::join(from_file.close(), to_file.close()).await;\n                    return Err(OperationError::from_state(state, &ctx.controller).into());\n                }\n            }\n\n            buf_in = buf_out;\n        }\n\n        ctx.buf = buf_in;\n\n        if let Some(metadata) = metadata.as_ref() {\n            let mut times = fs::FileTimes::new();\n            if let Ok(time) = metadata.modified() {\n                times = times.set_modified(time);\n            }\n            if let Ok(time) = metadata.accessed() {\n                times = times.set_accessed(time);\n            }\n            //TODO: upstream set_times implementation to compio?\n            let op = AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {\n                BufResult(file.set_times(times).map(|_| 0), ())\n            });\n            match compio::runtime::submit(op).await.0.map(|_| ()) {\n                Ok(()) => {\n                    tracing::info!(\"set times for {} to {:?}\", self.to.display(), times);\n                }\n                Err(why) => {\n                    if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {\n                        tracing::error!(?why, \"failed to set times for {}\", self.to.display());\n                    }\n                }\n            }\n        }\n\n        _ = to_file.close().await;\n\n        Ok(true)\n    }\n\n    /// Fallback mechanism in the event that unsupported I/O error errors occur.\n    /// Fixes unsupported errors when copying large files over MTP.\n    /// TODO: Find what Gio.File does to work around this.\n    #[cfg(feature = \"gvfs\")]\n    async fn gio_file_copy(\n        &self,\n        ctx: &mut Context,\n        mut progress: Progress,\n    ) -> Result<(), GioCopyError> {\n        _ = compio::fs::remove_file(&self.to).await;\n\n        let from = gio::File::for_path(&self.from);\n        let to = gio::File::for_path(&self.to);\n        let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel();\n        let (tx, rx) = tokio::sync::oneshot::channel();\n        let (pause_tx, mut pause_rx) = tokio::sync::watch::channel(false);\n\n        let task = compio::runtime::spawn_blocking(move || {\n            let glib_context = glib::MainContext::new();\n            let glib_loop = glib::MainLoop::new(Some(&glib_context), false);\n            glib_context.with_thread_default(move || {\n                let glib_loop2 = glib_loop.clone();\n                glib::MainContext::ref_thread_default().spawn_local(async move {\n                    // Create a future for copying the file with `gio::File`. This also creates a progress stream.\n                    let (gio_copy_fut, mut progress_stream) = from.copy_future(\n                        &to,\n                        gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::ALL_METADATA,\n                        glib::Priority::LOW,\n                    );\n\n                    let mut copy_fut = gio_copy_fut\n                        .map(|result| result.map_err(GioCopyError::GLib))\n                        .fuse();\n\n                    let progress_fut = std::pin::pin!(async {\n                        while let Some((current_bytes, _)) = progress_stream.next().await {\n                            _ = progress_tx.send(current_bytes);\n                        }\n\n                        drop(progress_tx);\n                        futures::future::pending::<()>().await;\n                    });\n\n                    let mut progress_fut = progress_fut.fuse();\n                    let mut pause_rx2 = pause_rx.clone();\n\n                    loop {\n                        let until_paused = std::pin::pin!(pause_rx.wait_for(|paused| *paused));\n                        futures::select! {\n                            _ = &mut progress_fut => {},\n\n                            result = &mut copy_fut => {\n                                _ = tx.send(result.map(|_| ()));\n                                glib_loop2.quit();\n                                return;\n                            }\n\n                            _ = until_paused.fuse() => {\n                                _ = pause_rx2.wait_for(|paused| !*paused).await;\n                            }\n                        }\n                    }\n                });\n\n                glib_loop.run();\n            })\n        });\n\n        let mut last_progress_update = Instant::now();\n        let mut task = task.fuse();\n        let mut rx = rx.fuse();\n\n        loop {\n            let until_paused = std::pin::pin!(ctx.controller.until_paused());\n            futures::select! {\n                value = progress_rx.recv().fuse() => {\n                    if let Some(current_bytes) = value {\n                        progress.current_bytes = current_bytes as u64;\n                        let current = Instant::now();\n                        if current.duration_since(last_progress_update).as_millis() > 49 {\n                            last_progress_update = current;\n                            (ctx.on_progress)(self, &progress);\n                            // Also check if the progress was cancelled.\n                            if let Err(state) = ctx.controller.check().await {\n                                tracing::warn!(\n                                    \"operation to copy from {:?} to {:?} cancelled\",\n                                    self.from,\n                                    self.to\n                                );\n                                return Err::<(), GioCopyError>(GioCopyError::Controller(\n                                    OperationError::from_state(state, &ctx.controller),\n                                ));\n                            }\n                        }\n                    }\n                }\n\n                result = rx => return result.unwrap(),\n\n                _ = task => (),\n\n                _ = until_paused.fuse() => {\n                    // Pauses an active copy while the controller state is paused.\n                    _ = pause_tx.send(true);\n                    ctx.controller.until_unpaused().await;\n                    _ = pause_tx.send(false);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/spawn_detached.rs",
    "content": "use std::{io, process};\n\n// This code is from the open crate and retains its MIT license.\npub fn spawn_detached(command: &mut process::Command) -> io::Result<()> {\n    command\n        .stdin(process::Stdio::null())\n        .stdout(process::Stdio::null())\n        .stderr(process::Stdio::null());\n\n    #[cfg(unix)]\n    unsafe {\n        use std::os::unix::process::CommandExt as _;\n\n        command\n            .pre_exec(move || {\n                match libc::fork() {\n                    -1 => return Err(io::Error::last_os_error()),\n                    0 => (),\n                    _ => libc::_exit(0),\n                }\n\n                if libc::setsid() == -1 {\n                    return Err(io::Error::last_os_error());\n                }\n\n                Ok(())\n            })\n            .spawn()?\n            .wait()\n            .map(|_| ())\n    }\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;\n        const CREATE_NO_WINDOW: u32 = 0x08000000;\n        command\n            .creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW)\n            .spawn()\n            .map(|_| ())\n    }\n}\n"
  },
  {
    "path": "src/tab.rs",
    "content": "#[cfg(feature = \"desktop\")]\nuse cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};\nuse cosmic::iced::advanced::graphics;\nuse cosmic::iced::advanced::text::{self, Paragraph};\nuse cosmic::iced::alignment::Vertical;\nuse cosmic::iced::clipboard::dnd::DndAction;\nuse cosmic::iced::core::mouse::ScrollDelta;\nuse cosmic::iced::core::widget::tree;\nuse cosmic::iced::futures::{self, SinkExt};\nuse cosmic::iced::keyboard::Modifiers;\nuse cosmic::iced::widget::scrollable::{self, AbsoluteOffset, Viewport};\nuse cosmic::iced::widget::{rule, stack};\nuse cosmic::iced::{\n    Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,\n    padding, stream, window,\n};\nuse cosmic::widget::menu::action::MenuAction;\nuse cosmic::widget::menu::key_bind::KeyBind;\nuse cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};\nuse cosmic::{Apply, Element, cosmic_theme, font, theme};\nuse i18n_embed::LanguageLoader;\nuse icu::datetime::input::DateTime;\nuse icu::datetime::options::TimePrecision;\nuse icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets};\nuse icu::locale::preferences::extensions::unicode::keywords::HourCycle;\nuse image::{DynamicImage, ImageReader};\nuse jiff_icu::ConvertFrom;\nuse mime_guess::{Mime, mime};\nuse rustc_hash::FxHashMap;\nuse serde::{Deserialize, Serialize};\nuse std::borrow::Cow;\nuse std::cell::Cell;\nuse std::cmp::{Ordering, Reverse};\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\nuse std::error::Error;\nuse std::fmt::{self, Display};\nuse std::fs::{self, File, Metadata};\nuse std::hash::Hash;\nuse std::io::{BufRead, BufReader};\n#[cfg(unix)]\nuse std::os::unix::fs::MetadataExt;\nuse std::path::{self, Path, PathBuf};\nuse std::sync::{Arc, LazyLock, RwLock, atomic};\nuse std::time::{Duration, Instant, SystemTime};\nuse tempfile::NamedTempFile;\nuse tokio::sync::mpsc;\nuse trash::{TrashItem, TrashItemMetadata, TrashItemSize};\nuse walkdir::WalkDir;\n\nuse crate::app::{Action, PreviewItem, PreviewKind};\nuse crate::clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste};\nuse crate::config::{\n    ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig,\n    ThumbCfg,\n};\nuse crate::dialog::DialogKind;\nuse crate::large_image::{\n    LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker,\n    should_use_tiling,\n};\nuse crate::localize::{LANGUAGE_SORTER, LOCALE};\nuse crate::mime_icon::{mime_for_path, mime_icon};\nuse crate::mounter::MOUNTERS;\nuse crate::operation::{Controller, OperationError};\nuse crate::thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize};\nuse crate::thumbnailer::thumbnailer;\nuse crate::trash::{Trash, TrashExt};\nuse crate::{FxOrderMap, fl, menu, mime_app, mouse_area};\n\npub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);\npub const HOVER_DURATION: Duration = Duration::from_millis(1600);\npub const TYPE_SELECT_TIMEOUT: Duration = Duration::from_millis(1000);\n//TODO: best limit for search items\nconst MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20);\nconst MAX_SEARCH_RESULTS: usize = 200;\n//TODO: configurable thumbnail size?\nconst THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);\n\n// Thumbnail generation semaphore - limits parallel thumbnail workers\n// Uses 4 workers for balanced throughput and memory usage\npub static THUMB_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =\n    LazyLock::new(|| tokio::sync::Semaphore::const_new(num_cpus::get().min(4)));\n\npub(crate) static SORT_OPTION_FALLBACK: LazyLock<FxHashMap<String, (HeadingOptions, bool)>> =\n    LazyLock::new(|| {\n        FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| {\n            (\n                Location::Path(dir).normalize().to_string(),\n                (HeadingOptions::Modified, false),\n            )\n        }))\n    });\n\nstatic MODE_NAMES: LazyLock<Vec<String>> = LazyLock::new(|| {\n    vec![\n        // Mode 0\n        fl!(\"none\"),\n        // Mode 1\n        fl!(\"execute-only\"),\n        // Mode 2\n        fl!(\"write-only\"),\n        // Mode 3\n        fl!(\"write-execute\"),\n        // Mode 4\n        fl!(\"read-only\"),\n        // Mode 5\n        fl!(\"read-execute\"),\n        // Mode 6\n        fl!(\"read-write\"),\n        // Mode 7\n        fl!(\"read-write-execute\"),\n    ]\n});\n\nstatic SPECIAL_DIRS: LazyLock<FxHashMap<PathBuf, &'static str>> = LazyLock::new(|| {\n    let mut special_dirs = FxHashMap::default();\n    if let Some(dir) = dirs::document_dir() {\n        special_dirs.insert(dir, \"folder-documents\");\n    }\n    if let Some(dir) = dirs::download_dir() {\n        special_dirs.insert(dir, \"folder-download\");\n    }\n    if let Some(dir) = dirs::audio_dir() {\n        special_dirs.insert(dir, \"folder-music\");\n    }\n    if let Some(dir) = dirs::picture_dir() {\n        special_dirs.insert(dir, \"folder-pictures\");\n    }\n    if let Some(dir) = dirs::public_dir() {\n        special_dirs.insert(dir, \"folder-publicshare\");\n    }\n    if let Some(dir) = dirs::template_dir() {\n        special_dirs.insert(dir, \"folder-templates\");\n    }\n    if let Some(dir) = dirs::video_dir() {\n        special_dirs.insert(dir, \"folder-videos\");\n    }\n    if let Some(dir) = dirs::desktop_dir() {\n        special_dirs.insert(dir, \"user-desktop\");\n    }\n    if let Some(dir) = dirs::home_dir() {\n        special_dirs.insert(dir, \"user-home\");\n    }\n    special_dirs\n});\n\nfn button_appearance(\n    theme: &theme::Theme,\n    selected: bool,\n    highlighted: bool,\n    cut: bool,\n    focused: bool,\n    accent: bool,\n    condensed_radius: bool,\n    desktop: bool,\n) -> widget::button::Style {\n    let cosmic = theme.cosmic();\n    let mut appearance = widget::button::Style::new();\n    if selected {\n        if accent {\n            appearance.background = Some(Color::from(cosmic.accent_color()).into());\n            appearance.icon_color = Some(Color::from(cosmic.on_accent_color()));\n            if cut {\n                appearance.text_color = Some(Color::from(cosmic.accent.on_disabled));\n            } else {\n                appearance.text_color = Some(Color::from(cosmic.on_accent_color()));\n            }\n        } else {\n            appearance.background = Some(Color::from(cosmic.bg_component_color()).into());\n        }\n    } else if highlighted {\n        if accent {\n            appearance.background = Some(Color::from(cosmic.bg_component_color()).into());\n            appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color()));\n            appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));\n            if cut {\n                appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled));\n            } else {\n                appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));\n            }\n        } else {\n            appearance.background = Some(Color::from(cosmic.bg_component_color()).into());\n        }\n    } else if desktop {\n        appearance.background = Some(Color::from(cosmic.bg_color()).into());\n        appearance.icon_color = Some(Color::from(cosmic.on_bg_color()));\n        if cut {\n            appearance.text_color = Some(Color::from(cosmic.background.component.disabled));\n        } else {\n            appearance.text_color = Some(Color::from(cosmic.on_bg_color()));\n        }\n    } else if cut {\n        appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled));\n    }\n    if focused && accent {\n        appearance.outline_width = 1.0;\n        appearance.outline_color = Color::from(cosmic.accent_color());\n        appearance.border_width = 2.0;\n        appearance.border_color = Color::TRANSPARENT;\n    }\n    if condensed_radius {\n        appearance.border_radius = cosmic.radius_xs().into();\n    } else {\n        appearance.border_radius = cosmic.radius_s().into();\n    }\n    appearance\n}\n\nfn button_style(\n    selected: bool,\n    highlighted: bool,\n    cut: bool,\n    accent: bool,\n    condensed_radius: bool,\n    desktop: bool,\n) -> theme::Button {\n    //TODO: move to libcosmic?\n    theme::Button::Custom {\n        active: Box::new(move |focused, theme| {\n            button_appearance(\n                theme,\n                selected,\n                highlighted,\n                cut,\n                focused,\n                accent,\n                condensed_radius,\n                desktop,\n            )\n        }),\n        disabled: Box::new(move |theme| {\n            button_appearance(\n                theme,\n                selected,\n                highlighted,\n                cut,\n                false,\n                accent,\n                condensed_radius,\n                desktop,\n            )\n        }),\n        hovered: Box::new(move |focused, theme| {\n            button_appearance(\n                theme,\n                selected,\n                highlighted,\n                cut,\n                focused,\n                accent,\n                condensed_radius,\n                desktop,\n            )\n        }),\n        pressed: Box::new(move |focused, theme| {\n            button_appearance(\n                theme,\n                selected,\n                highlighted,\n                cut,\n                focused,\n                accent,\n                condensed_radius,\n                desktop,\n            )\n        }),\n    }\n}\n\npub fn folder_icon(path: &PathBuf, icon_size: u16) -> widget::icon::Handle {\n    widget::icon::from_name(SPECIAL_DIRS.get(path).map_or(\"folder\", |x| *x))\n        .size(icon_size)\n        .handle()\n}\n\npub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Handle {\n    widget::icon::from_name(format!(\n        \"{}-symbolic\",\n        SPECIAL_DIRS.get(path).map_or(\"folder\", |x| *x)\n    ))\n    .size(icon_size)\n    .handle()\n}\n\n//TODO: replace with Path::has_trailing_sep when stable\nfn has_trailing_sep(path: &Path) -> bool {\n    path.as_os_str()\n        .as_encoded_bytes()\n        .last()\n        .copied()\n        .is_some_and(|b| path::is_separator(b as char))\n}\n\nfn tab_complete(path: &Path) -> Result<Vec<(String, PathBuf)>, Box<dyn Error>> {\n    let parent = if has_trailing_sep(path) && path.is_dir() {\n        // Show completions inside existing child directory instead of parent\n        path\n    } else {\n        path.parent()\n            .ok_or_else(|| format!(\"path has no parent {}\", path.display()))?\n    };\n\n    let child_os = path.strip_prefix(parent)?;\n    let child = child_os\n        .to_str()\n        .ok_or_else(|| format!(\"invalid UTF-8 {}\", child_os.display()))?;\n\n    let pattern = format!(\"^{}\", regex::escape(child));\n    let regex = regex::RegexBuilder::new(&pattern)\n        .case_insensitive(true)\n        .build()?;\n\n    let mut completions = Vec::new();\n    for entry_res in fs::read_dir(parent)? {\n        let entry = entry_res?;\n        let file_name_os = entry.file_name();\n        let Some(file_name) = file_name_os.to_str() else {\n            continue;\n        };\n        // Don't list hidden files before entering a pattern\n        if pattern == \"^\" && file_name.starts_with('.') {\n            continue;\n        }\n        if regex.is_match(file_name) {\n            completions.push((file_name.to_string(), entry.path()));\n        }\n    }\n\n    completions.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.0, &b.0));\n    //TODO: make the list scrollable?\n    completions.truncate(8);\n    Ok(completions)\n}\n\n//TODO: translate, add more levels?\nfn format_size(size: u64) -> String {\n    const KB: u64 = 1000;\n    const MB: u64 = 1000 * KB;\n    const GB: u64 = 1000 * MB;\n    const TB: u64 = 1000 * GB;\n\n    if size >= TB {\n        format!(\"{:.1} TB\", size as f64 / TB as f64)\n    } else if size >= GB {\n        format!(\"{:.1} GB\", size as f64 / GB as f64)\n    } else if size >= MB {\n        format!(\"{:.1} MB\", size as f64 / MB as f64)\n    } else if size >= KB {\n        format!(\"{:.1} KB\", size as f64 / KB as f64)\n    } else {\n        format!(\"{size} B\")\n    }\n}\n\nconst MODE_SHIFT_USER: u32 = 6;\nconst MODE_SHIFT_GROUP: u32 = 3;\nconst MODE_SHIFT_OTHER: u32 = 0;\n\nconst fn get_mode_part(mode: u32, shift: u32) -> u32 {\n    (mode >> shift) & 0o7\n}\n\nfn set_mode_part(mode: u32, shift: u32, bits: u32) -> u32 {\n    assert!(bits <= 0o7);\n    (mode & !(0o7 << shift)) | (bits << shift)\n}\n\nfn date_time_formatter(military_time: bool) -> DateTimeFormatter<fieldsets::YMDT> {\n    let mut prefs = DateTimeFormatterPreferences::from(LOCALE.clone());\n    prefs.hour_cycle = Some(if military_time {\n        HourCycle::H23\n    } else {\n        HourCycle::H12\n    });\n\n    let mut fs = fieldsets::YMDT::medium();\n    fs = fs.with_time_precision(TimePrecision::Minute);\n\n    DateTimeFormatter::try_new(prefs, fs).expect(\"failed to create DateTimeFormatter\")\n}\n\nfn time_formatter(military_time: bool) -> DateTimeFormatter<fieldsets::T> {\n    let mut prefs = DateTimeFormatterPreferences::from(LOCALE.clone());\n    prefs.hour_cycle = Some(if military_time {\n        HourCycle::H23\n    } else {\n        HourCycle::H12\n    });\n\n    let mut fs = fieldsets::T::medium();\n    fs = fs.with_time_precision(TimePrecision::Minute);\n\n    DateTimeFormatter::try_new(prefs, fs).expect(\"failed to create DateTimeFormatter\")\n}\n\nstruct FormatTime<'a> {\n    pub time: SystemTime,\n    pub date_time_formatter: &'a DateTimeFormatter<fieldsets::YMDT>,\n    pub time_formatter: &'a DateTimeFormatter<fieldsets::T>,\n}\n\nimpl<'a> FormatTime<'a> {\n    fn from_secs(\n        secs: i64,\n        date_time_formatter: &'a DateTimeFormatter<fieldsets::YMDT>,\n        time_formatter: &'a DateTimeFormatter<fieldsets::T>,\n    ) -> Option<Self> {\n        // This looks convoluted because we need to ensure the units match up\n        let secs: u64 = secs.try_into().ok()?;\n        let now = SystemTime::now();\n        let filetime_diff = now\n            .duration_since(SystemTime::UNIX_EPOCH)\n            .map(|from_epoch| from_epoch.as_secs())\n            .ok()\n            .and_then(|now_secs| now_secs.checked_sub(secs))\n            .map(Duration::from_secs)?;\n        now.checked_sub(filetime_diff).map(|time| Self {\n            time,\n            date_time_formatter,\n            time_formatter,\n        })\n    }\n}\n\nimpl Display for FormatTime<'_> {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let zoned = jiff::Zoned::try_from(self.time).unwrap();\n        let now = jiff::Zoned::now();\n        let icu_datetime = DateTime::convert_from(zoned.datetime());\n        if zoned.date() == now.date() {\n            f.write_str(fl!(\"today\").as_str())?;\n            f.write_str(\", \")?;\n            self.time_formatter.format(&icu_datetime).fmt(f)\n        } else {\n            self.date_time_formatter.format(&icu_datetime).fmt(f)\n        }\n    }\n}\n\nconst fn format_time<'a>(\n    time: SystemTime,\n    date_time_formatter: &'a DateTimeFormatter<fieldsets::YMDT>,\n    time_formatter: &'a DateTimeFormatter<fieldsets::T>,\n) -> FormatTime<'a> {\n    FormatTime {\n        time,\n        date_time_formatter,\n        time_formatter,\n    }\n}\n\n#[cfg(not(target_os = \"windows\"))]\nfn hidden_attribute(_metadata: &Metadata) -> bool {\n    false\n}\n\n#[cfg(target_os = \"windows\")]\nfn hidden_attribute(metadata: &Metadata) -> bool {\n    use std::os::windows::fs::MetadataExt;\n    // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants\n    const FILE_ATTRIBUTE_HIDDEN: u32 = 2;\n    metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum FsKind {\n    Local,\n    Remote,\n    Gvfs,\n}\n\n#[cfg(target_os = \"linux\")]\npub fn fs_kind(metadata: &Metadata) -> FsKind {\n    //TODO: method to reload remote filesystems dynamically\n    //TODO: fix for https://github.com/eminence/procfs/issues/262\n    static DEVICES: LazyLock<FxHashMap<u64, FsKind>> = LazyLock::new(|| {\n        let mut devices = FxHashMap::default();\n        match procfs::process::Process::myself() {\n            Ok(process) => match process.mountinfo() {\n                Ok(mount_infos) => {\n                    devices = FxHashMap::from_iter(mount_infos.iter().filter_map(|mount_info| {\n                        let mut parts = mount_info.majmin.split(':');\n                        let major_str = parts.next()?;\n                        let minor_str = parts.next()?;\n                        let major = major_str.parse::<libc::c_uint>().ok()?;\n                        let minor = minor_str.parse::<libc::c_uint>().ok()?;\n                        let dev = libc::makedev(major, minor);\n                        // Network and distributed filesystem types\n                        // Based on common remote filesystem types found in /proc/mounts\n                        let kind = match mount_info.fs_type.as_str() {\n                            // SMB/CIFS variants\n                            \"cifs\" | \"smb\" | \"smb2\" | \"smbfs\" => FsKind::Remote,\n\n                            // NFS variants\n                            \"nfs\" | \"nfs4\" => FsKind::Remote,\n\n                            // FUSE-based remote filesystems\n                            \"fuse.rclone\" | \"fuse.sshfs\" | \"fuse.davfs2\" | \"fuse.ceph\"\n                            | \"fuse.glusterfs\" | \"fuse.s3fs\" | \"fuse.goofys\" | \"fuse.gcsfuse\"\n                            | \"fuse.afp\" | \"fuse.afpfs\" => FsKind::Remote,\n\n                            // Other network protocols\n                            \"afs\" | \"coda\" | \"ncpfs\" | \"davfs\" | \"davfs2\" | \"shfs\" => {\n                                FsKind::Remote\n                            }\n\n                            // Cluster/distributed filesystems\n                            \"ceph\" | \"glusterfs\" | \"lustre\" | \"gfs\" | \"gfs2\" | \"ocfs2\" => {\n                                FsKind::Remote\n                            }\n\n                            // GVFS (GNOME Virtual File System)\n                            \"fuse.gvfsd-fuse\" => FsKind::Gvfs,\n\n                            // Everything else is local\n                            _ => FsKind::Local,\n                        };\n                        Some((dev, kind))\n                    }));\n                }\n                Err(err) => {\n                    log::warn!(\"failed to get mount info: {err}\");\n                }\n            },\n            Err(err) => {\n                log::warn!(\"failed to get process info: {err}\");\n            }\n        }\n        devices\n    });\n    DEVICES.get(&metadata.dev()).map_or(FsKind::Local, |x| *x)\n}\n\n#[cfg(not(target_os = \"linux\"))]\npub fn fs_kind(_metadata: &Metadata) -> FsKind {\n    //TODO: support BSD, macOS, Windows?\n    FsKind::Local\n}\n\n#[cfg(not(feature = \"desktop\"))]\nfn get_desktop_file_display_name(path: &Path) -> Option<String> {\n    None\n}\n\n#[cfg(feature = \"desktop\")]\nfn get_desktop_file_display_name(path: &Path) -> Option<String> {\n    let locales = get_languages_from_env();\n    let entry = match DesktopEntry::from_path(path, Some(&locales)) {\n        Ok(ok) => ok,\n        Err(err) => {\n            log::warn!(\"failed to parse {}: {}\", path.display(), err);\n            return None;\n        }\n    };\n\n    entry.name(&locales).map(|s| s.into_owned())\n}\n\n#[cfg(not(feature = \"desktop\"))]\nfn get_desktop_file_icon(path: &Path) -> Option<String> {\n    None\n}\n\n#[cfg(feature = \"desktop\")]\nfn get_desktop_file_icon(path: &Path) -> Option<String> {\n    let entry = match DesktopEntry::from_path::<&str>(path, None) {\n        Ok(ok) => ok,\n        Err(err) => {\n            log::warn!(\"failed to parse {}: {}\", path.display(), err);\n            return None;\n        }\n    };\n\n    entry.icon().map(str::to_string)\n}\n\n/// Creates an icon handle from a desktop file's Icon field value.\n/// Supports both icon names (looked up in theme) and absolute paths (used directly).\nfn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle {\n    let icon_path = Path::new(icon);\n    if icon_path.is_absolute() && icon_path.exists() {\n        widget::icon::from_path(icon_path.to_path_buf())\n    } else {\n        widget::icon::from_name(icon).size(size).handle()\n    }\n}\n\n#[cfg(feature = \"desktop\")]\npub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {\n    let locales = get_languages_from_env();\n    let entry = match DesktopEntry::from_path(path, Some(&locales)) {\n        Ok(ok) => ok,\n        Err(err) => {\n            log::warn!(\"failed to parse {}: {}\", path.display(), err);\n            return (None, None);\n        }\n    };\n    (\n        entry.name(&locales).map(|s| s.into_owned()),\n        entry.icon().map(str::to_string),\n    )\n}\n\nfn display_name_for_file(path: &Path, name: &str, get_from_gvfs: bool, is_desktop: bool) -> String {\n    if is_desktop {\n        return get_desktop_file_display_name(path).map_or_else(\n            || Item::display_name(name),\n            |desktop_name| Item::display_name(desktop_name.as_str()),\n        );\n    } else if get_from_gvfs {\n        #[cfg(feature = \"gvfs\")]\n        {\n            let file = gio::File::for_path(path);\n            if let Ok(info) = gio::prelude::FileExt::query_info(\n                &file,\n                \"standard::display-name\",\n                gio::FileQueryInfoFlags::NONE,\n                gio::Cancellable::NONE,\n            ) {\n                return Item::display_name(info.display_name().as_str());\n            }\n        }\n    }\n    Item::display_name(name)\n}\n\n#[cfg(feature = \"gvfs\")]\npub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item {\n    let file_name = file_info\n        .attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_NAME)\n        .unwrap_or_default();\n    let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);\n    let mut is_desktop = false;\n    let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE);\n    let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);\n\n    let size_opt = (!is_dir).then_some(file_info.size() as u64);\n\n    let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if is_dir {\n        (\n            //TODO: make this a static\n            \"inode/directory\".parse().unwrap(),\n            folder_icon(&path, sizes.grid()),\n            folder_icon(&path, sizes.list()),\n            folder_icon(&path, sizes.list_condensed()),\n        )\n    } else {\n        // ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive\n        // @todo - expose this as a config option?\n        let mime = mime_for_path(&path, None, true);\n\n        //TODO: clean this up, implement for trash\n        let icon_name_opt = if mime == \"application/x-desktop\" {\n            is_desktop = true;\n            get_desktop_file_icon(&path)\n        } else {\n            None\n        };\n        if let Some(icon_name) = icon_name_opt {\n            (\n                mime,\n                desktop_icon_handle(&icon_name, sizes.grid()),\n                desktop_icon_handle(&icon_name, sizes.list()),\n                desktop_icon_handle(&icon_name, sizes.list_condensed()),\n            )\n        } else {\n            (\n                mime.clone(),\n                mime_icon(mime.clone(), sizes.grid()),\n                mime_icon(mime.clone(), sizes.list()),\n                mime_icon(mime, sizes.list_condensed()),\n            )\n        }\n    };\n\n    let mut children_opt = None;\n    let mut dir_size = DirSize::NotDirectory;\n    if is_dir && !remote {\n        dir_size = DirSize::Calculating(Controller::default());\n        //TODO: calculate children in the background (and make it cancellable?)\n        match fs::read_dir(&path) {\n            Ok(entries) => {\n                children_opt = Some(entries.count());\n            }\n            Err(err) => {\n                log::warn!(\"failed to read directory {}: {}\", path.display(), err);\n            }\n        }\n    }\n\n    let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);\n    let hidden = file_name.starts_with('.');\n\n    Item {\n        name: file_name.into(),\n        display_name,\n        is_mount_point: false,\n        metadata: ItemMetadata::GvfsPath {\n            mtime,\n            size_opt,\n            children_opt,\n        },\n        hidden,\n        image_dimensions: (!remote && mime.type_() == mime::IMAGE)\n            .then(|| image::image_dimensions(&path).ok())\n            .flatten(),\n        location_opt: Some(Location::Path(path)),\n        mime,\n        icon_handle_grid,\n        icon_handle_list,\n        icon_handle_list_condensed,\n        thumbnail_opt: if remote {\n            Some(ItemThumbnail::NotImage)\n        } else {\n            None\n        },\n        button_id: widget::Id::unique(),\n        pos_opt: Cell::new(None),\n        rect_opt: Cell::new(None),\n        selected: false,\n        highlighted: false,\n        overlaps_drag_rect: false,\n        dir_size,\n        cut: false,\n    }\n}\n\npub fn item_from_search_item(search_item: SearchItem, sizes: IconSizes) -> Item {\n    match search_item {\n        SearchItem::Path(path, name, metadata) => item_from_entry(path, name, metadata, sizes),\n        SearchItem::Trash(entry, metadata) => item_from_trash_entry(entry, metadata, sizes),\n    }\n}\n\npub fn item_from_entry(\n    path: PathBuf,\n    name: String,\n    metadata: fs::Metadata,\n    sizes: IconSizes,\n) -> Item {\n    let mut is_desktop = false;\n    let mut is_gvfs = false;\n\n    let hidden = name.starts_with('.') || hidden_attribute(&metadata);\n\n    let remote = match fs_kind(&metadata) {\n        FsKind::Local => false,\n        FsKind::Remote => true,\n        #[cfg(feature = \"gvfs\")]\n        FsKind::Gvfs => {\n            is_gvfs = true;\n            let file = gio::File::for_path(&path);\n\n            match gio::prelude::FileExt::query_filesystem_info(\n                &file,\n                gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,\n                gio::Cancellable::NONE,\n            ) {\n                Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),\n                Err(err) => {\n                    log::warn!(\n                        \"failed to get GIO filesystem info for {}: {}\",\n                        path.display(),\n                        err\n                    );\n                    true\n                }\n            }\n        }\n        #[cfg(not(feature = \"gvfs\"))]\n        FsKind::Gvfs => {\n            log::info!(\n                \"gvfs feature not enabled, info may be inaccurate for {}\",\n                path.display()\n            );\n            true\n        }\n    };\n\n    let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =\n        if metadata.is_dir() {\n            (\n                //TODO: make this a static\n                \"inode/directory\".parse().unwrap(),\n                folder_icon(&path, sizes.grid()),\n                folder_icon(&path, sizes.list()),\n                folder_icon(&path, sizes.list_condensed()),\n            )\n        } else {\n            let mime = mime_for_path(&path, Some(&metadata), remote);\n            //TODO: clean this up, implement for trash\n            let icon_name_opt = if mime == \"application/x-desktop\" {\n                is_desktop = true;\n                get_desktop_file_icon(&path)\n            } else {\n                None\n            };\n            if let Some(icon_name) = icon_name_opt {\n                (\n                    mime,\n                    desktop_icon_handle(&icon_name, sizes.grid()),\n                    desktop_icon_handle(&icon_name, sizes.list()),\n                    desktop_icon_handle(&icon_name, sizes.list_condensed()),\n                )\n            } else {\n                (\n                    mime.clone(),\n                    mime_icon(mime.clone(), sizes.grid()),\n                    mime_icon(mime.clone(), sizes.list()),\n                    mime_icon(mime, sizes.list_condensed()),\n                )\n            }\n        };\n\n    let mut children_opt = None;\n    let mut dir_size = DirSize::NotDirectory;\n    if metadata.is_dir() && !remote {\n        dir_size = DirSize::Calculating(Controller::default());\n        //TODO: calculate children in the background (and make it cancellable?)\n        match fs::read_dir(&path) {\n            Ok(entries) => {\n                children_opt = Some(entries.count());\n            }\n            Err(err) => {\n                log::warn!(\"failed to read directory {}: {}\", path.display(), err);\n            }\n        }\n    }\n\n    let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);\n\n    Item {\n        name,\n        display_name,\n        is_mount_point: false,\n        metadata: ItemMetadata::Path {\n            metadata,\n            children_opt,\n        },\n        hidden,\n        location_opt: Some(Location::Path(path)),\n        image_dimensions: None,\n        mime,\n        icon_handle_grid,\n        icon_handle_list,\n        icon_handle_list_condensed,\n        thumbnail_opt: remote.then_some(ItemThumbnail::NotImage),\n        button_id: widget::Id::unique(),\n        pos_opt: Cell::new(None),\n        rect_opt: Cell::new(None),\n        selected: false,\n        highlighted: false,\n        overlaps_drag_rect: false,\n        dir_size,\n        cut: false,\n    }\n}\n\npub fn item_from_trash_entry(\n    entry: TrashItem,\n    metadata: TrashItemMetadata,\n    sizes: IconSizes,\n) -> Item {\n    let original_path = entry.original_path();\n    let name = entry.name.to_string_lossy().into_owned();\n    let display_name = Item::display_name(&name);\n\n    let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size\n    {\n        trash::TrashItemSize::Entries(_) => (\n            //TODO: make this a static\n            \"inode/directory\".parse().unwrap(),\n            folder_icon(&original_path, sizes.grid()),\n            folder_icon(&original_path, sizes.list()),\n            folder_icon(&original_path, sizes.list_condensed()),\n        ),\n        trash::TrashItemSize::Bytes(_) => {\n            // This passes remote = true so it does not read from the original path\n            let mime = mime_for_path(&original_path, None, true);\n            (\n                mime.clone(),\n                mime_icon(mime.clone(), sizes.grid()),\n                mime_icon(mime.clone(), sizes.list()),\n                mime_icon(mime, sizes.list_condensed()),\n            )\n        }\n    };\n\n    Item {\n        name,\n        display_name,\n        is_mount_point: false,\n        metadata: ItemMetadata::Trash { metadata, entry },\n        hidden: false,\n        location_opt: None,\n        image_dimensions: (mime.type_() == mime::IMAGE)\n            .then(|| image::image_dimensions(&original_path).ok())\n            .flatten(),\n        mime,\n        icon_handle_grid,\n        icon_handle_list,\n        icon_handle_list_condensed,\n        thumbnail_opt: Some(ItemThumbnail::NotImage),\n        button_id: widget::Id::unique(),\n        pos_opt: Cell::new(None),\n        rect_opt: Cell::new(None),\n        selected: false,\n        highlighted: false,\n        overlaps_drag_rect: false,\n        dir_size: DirSize::NotDirectory,\n        cut: false,\n    }\n}\n\nfn get_filename_from_path(path: &Path) -> Result<String, String> {\n    Ok(match path.file_name() {\n        Some(name_os) => name_os\n            .to_str()\n            .ok_or_else(|| {\n                format!(\n                    \"failed to parse file name for {}: {name_os:?} is not valid UTF-8\",\n                    path.display()\n                )\n            })?\n            .to_string(),\n        None => fl!(\"filesystem\"),\n    })\n}\n\npub fn item_from_path<P: Into<PathBuf>>(path: P, sizes: IconSizes) -> Result<Item, String> {\n    let path = path.into();\n    let name = get_filename_from_path(&path)?;\n    let metadata = fs::metadata(&path)\n        .map_err(|err| format!(\"failed to read metadata for {}: {}\", path.display(), err))?;\n    Ok(item_from_entry(path, name, metadata, sizes))\n}\n\npub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {\n    let mut items = Vec::new();\n    let mut hidden_files = Box::from([]);\n    let mut remote_scannable = false;\n\n    #[cfg(feature = \"gvfs\")]\n    {\n        if let Ok(path_meta) = fs::metadata(tab_path)\n            && fs_kind(&path_meta) == FsKind::Gvfs\n        {\n            let file = gio::File::for_path(tab_path);\n\n            // gio crate expects a comma delimited string\n            let attr_string = [\n                gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),\n                gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),\n                gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),\n                gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),\n                gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),\n                gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),\n            ]\n            .join(\",\");\n\n            match gio::prelude::FileExt::enumerate_children(\n                &file,\n                attr_string.as_str(),\n                gio::FileQueryInfoFlags::NONE,\n                gio::Cancellable::NONE,\n            ) {\n                Ok(res) => {\n                    remote_scannable = true;\n                    items = res\n                        .filter_map(|file| {\n                            let file = file.ok()?;\n                            Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes))\n                        })\n                        .collect();\n                }\n                Err(err) => {\n                    log::warn!(\n                        \"could not enumerate {} via gio: {}\",\n                        tab_path.display(),\n                        err\n                    );\n                }\n            }\n        }\n    }\n\n    if !remote_scannable {\n        match fs::read_dir(tab_path) {\n            Ok(entries) => {\n                items = entries\n                    .filter_map(|entry_res| {\n                        let entry = entry_res\n                            .inspect_err(|err| {\n                                log::warn!(\n                                    \"failed to read entry in {}: {}\",\n                                    tab_path.display(),\n                                    err\n                                )\n                            })\n                            .ok()?;\n\n                        let path = entry.path();\n\n                        let name = entry\n                            .file_name()\n                            .into_string()\n                            .inspect_err(|name_os| {\n                                log::warn!(\n                                    \"failed to parse entry at {}: {:?} is not valid UTF-8\",\n                                    path.display(),\n                                    name_os\n                                )\n                            })\n                            .ok()?;\n\n                        if name == \".hidden\" && path.is_file() {\n                            hidden_files = parse_hidden_file(&path);\n                        }\n\n                        let metadata = fs::metadata(&path)\n                            .inspect_err(|err| {\n                                log::warn!(\n                                    \"failed to read metadata for entry at {}: {}\",\n                                    path.display(),\n                                    err\n                                )\n                            })\n                            .ok()?;\n\n                        Some(item_from_entry(path, name, metadata, sizes))\n                    })\n                    .collect();\n            }\n            Err(err) => {\n                log::warn!(\"failed to read directory {}: {}\", tab_path.display(), err);\n            }\n        }\n    }\n    items.sort_unstable_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {\n        (true, false) => Ordering::Less,\n        (false, true) => Ordering::Greater,\n        _ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),\n    });\n    for item in &mut items {\n        if hidden_files.contains(&item.name) {\n            item.hidden = true;\n        }\n    }\n    items\n}\n\npub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(\n    search_location: &SearchLocation,\n    term: &str,\n    show_hidden: bool,\n    callback: F,\n) {\n    if term.is_empty() {\n        return;\n    }\n\n    let pattern = regex::escape(term);\n    let regex = match regex::RegexBuilder::new(&pattern)\n        .case_insensitive(true)\n        .build()\n    {\n        Ok(ok) => ok,\n        Err(err) => {\n            log::warn!(\"failed to parse regex {pattern:?}: {err}\");\n            return;\n        }\n    };\n\n    match search_location {\n        SearchLocation::Path(tab_path) => {\n            ignore::WalkBuilder::new(tab_path)\n                .standard_filters(false)\n                .hidden(!show_hidden)\n                //TODO: only use this on supported targets\n                .same_file_system(true)\n                .build_parallel()\n                .run(|| {\n                    Box::new(|entry_res| {\n                        let Ok(entry) = entry_res else {\n                            // Skip invalid entries\n                            return ignore::WalkState::Skip;\n                        };\n\n                        let Some(file_name) = entry.file_name().to_str() else {\n                            // Skip anything with an invalid name\n                            return ignore::WalkState::Skip;\n                        };\n\n                        if regex.is_match(file_name) {\n                            let path = entry.path();\n\n                            let metadata = match entry.metadata() {\n                                Ok(ok) => ok,\n                                Err(err) => {\n                                    log::warn!(\n                                        \"failed to read metadata for entry at {}: {}\",\n                                        path.display(),\n                                        err\n                                    );\n                                    return ignore::WalkState::Continue;\n                                }\n                            };\n\n                            if !callback(SearchItem::Path(\n                                path.to_path_buf(),\n                                file_name.to_string(),\n                                metadata,\n                            )) {\n                                return ignore::WalkState::Quit;\n                            }\n                        }\n\n                        ignore::WalkState::Continue\n                    })\n                });\n        }\n        SearchLocation::Recents => {\n            let recent_files = match recently_used_xbel::parse_file() {\n                Ok(recent_files) => recent_files,\n                Err(err) => {\n                    log::warn!(\"Error reading recent files: {err:?}\");\n                    return;\n                }\n            };\n\n            for bookmark in recent_files.bookmarks {\n                let path = uri_to_path(bookmark.href);\n                if let Some(path) = path\n                    && path.exists()\n                {\n                    let file_name = path.file_name();\n                    if let Some(file_name) = file_name {\n                        let file_name = file_name.to_string_lossy();\n                        if regex.is_match(&file_name) {\n                            match path.metadata() {\n                                Ok(metadata) => {\n                                    if !callback(SearchItem::Path(\n                                        path.to_path_buf(),\n                                        file_name.to_string(),\n                                        metadata,\n                                    )) {\n                                        break;\n                                    }\n                                }\n                                Err(err) => {\n                                    log::warn!(\n                                        \"failed to read metadata for entry at {}: {}\",\n                                        path.display(),\n                                        err\n                                    );\n                                }\n                            };\n                        }\n                    }\n                }\n            }\n        }\n        SearchLocation::Trash => {\n            Trash::scan_search(callback, &regex);\n        }\n    }\n}\n\nfn uri_to_path(uri: String) -> Option<PathBuf> {\n    uri.parse::<url::Url>().ok().and_then(|url| {\n        //TODO support for external drive or cloud?\n        if url.scheme() == \"file\" {\n            url.to_file_path().ok()\n        } else {\n            None\n        }\n    })\n}\n\npub fn has_recents() -> bool {\n    match recently_used_xbel::parse_file() {\n        Ok(recent_files) => !recent_files.bookmarks.is_empty(),\n        Err(_) => false,\n    }\n}\n\npub fn scan_recents(sizes: IconSizes) -> Vec<Item> {\n    let recent_files = match recently_used_xbel::parse_file() {\n        Ok(recent_files) => recent_files,\n        Err(err) => {\n            log::warn!(\"Error reading recent files: {err:?}\");\n            return Vec::new();\n        }\n    };\n    let mut recents: Vec<_> = recent_files\n        .bookmarks\n        .into_iter()\n        .filter_map(|bookmark| {\n            let path = uri_to_path(bookmark.href)?;\n            let last_edit = bookmark.modified.parse::<jiff::Timestamp>().ok()?;\n            let last_visit = bookmark.visited.parse::<jiff::Timestamp>().ok()?;\n\n            if path.exists() {\n                let file_name = path.file_name()?;\n                let name = file_name.to_string_lossy().to_string();\n\n                let metadata = match path.metadata() {\n                    Ok(ok) => ok,\n                    Err(err) => {\n                        log::warn!(\n                            \"failed to read metadata for entry at {}: {}\",\n                            path.display(),\n                            err\n                        );\n                        return None;\n                    }\n                };\n\n                let item = item_from_entry(path, name, metadata, sizes);\n                Some((item, last_edit.min(last_visit)))\n            } else {\n                log::warn!(\"recent file path does not exist: {}\", path.display());\n                None\n            }\n        })\n        .collect();\n\n    recents.sort_by_key(|recent| Reverse(recent.1));\n\n    recents.into_iter().take(50).map(|(item, _)| item).collect()\n}\n\npub fn scan_network(uri: &str, sizes: IconSizes) -> Vec<Item> {\n    for mounter in MOUNTERS.values() {\n        match mounter.network_scan(uri, sizes) {\n            Some(Ok(items)) => return items,\n            Some(Err(err)) => {\n                log::warn!(\"failed to scan {uri:?}: {err}\");\n            }\n            None => {}\n        }\n    }\n    Vec::new()\n}\n\n//TODO: organize desktop items based on display\npub fn scan_desktop(\n    tab_path: &PathBuf,\n    _display: &str,\n    desktop_config: DesktopConfig,\n    mut sizes: IconSizes,\n) -> Vec<Item> {\n    sizes.grid = desktop_config.icon_size;\n\n    let mut items = Vec::new();\n\n    if desktop_config.show_content {\n        items.extend(scan_path(tab_path, sizes));\n    }\n\n    if desktop_config.show_mounted_drives {\n        for mounter in MOUNTERS.values() {\n            let Some(mounter_items) = mounter.items(sizes) else {\n                continue;\n            };\n            items.extend(mounter_items.into_iter().filter_map(|mounter_item| {\n                let path = mounter_item.path()?;\n                // Get most item data from path\n                let mut item = match item_from_path(&path, sizes) {\n                    Ok(item) => item,\n                    Err(err) => {\n                        log::warn!(\n                            \"failed to get item from mounter item {}: {}\",\n                            path.display(),\n                            err\n                        );\n                        return None;\n                    }\n                };\n\n                //Override some data with mounter information\n                item.name = mounter_item.name();\n                item.display_name = Item::display_name(&item.name);\n\n                //TODO: use icon size for mounter item icon\n                if let Some(icon) = mounter_item.icon(false) {\n                    item.icon_handle_grid.clone_from(&icon);\n                    item.icon_handle_list.clone_from(&icon);\n                    item.icon_handle_list_condensed = icon;\n                }\n\n                Some(item)\n            }));\n        }\n    }\n\n    if desktop_config.show_trash {\n        let name = fl!(\"trash\");\n        let display_name = Item::display_name(&name);\n\n        let metadata = ItemMetadata::SimpleDir {\n            entries: Trash::entries() as u64,\n        };\n\n        let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {\n            (\n                \"inode/directory\".parse().unwrap(),\n                Trash::icon(sizes.grid()),\n                Trash::icon(sizes.list()),\n                Trash::icon(sizes.list_condensed()),\n            )\n        };\n\n        items.push(Item {\n            name,\n            display_name,\n            is_mount_point: false,\n            metadata,\n            hidden: false,\n            location_opt: Some(Location::Trash),\n            image_dimensions: None,\n            mime,\n            icon_handle_grid,\n            icon_handle_list,\n            icon_handle_list_condensed,\n            thumbnail_opt: Some(ItemThumbnail::NotImage),\n            button_id: widget::Id::unique(),\n            pos_opt: Cell::new(None),\n            rect_opt: Cell::new(None),\n            selected: false,\n            highlighted: false,\n            overlaps_drag_rect: false,\n            dir_size: DirSize::NotDirectory,\n            cut: false,\n        });\n    }\n\n    items\n}\n\n#[derive(Clone, Debug)]\npub struct EditLocation {\n    pub location: Location,\n    pub completions: Option<Vec<(String, PathBuf)>>,\n    pub selected: Option<usize>,\n}\n\nimpl EditLocation {\n    pub fn resolve(&self) -> Option<Location> {\n        if let Location::Network(uri, ..) = &self.location {\n            MOUNTERS\n                .values()\n                .find_map(|mounter| mounter.dir_info(uri))\n                .map(|(uri, display_name, path_opt)| Location::Network(uri, display_name, path_opt))\n        } else {\n            let Some(selected) = self.selected else {\n                return Some(self.location.clone());\n            };\n            let completions = self.completions.as_ref()?;\n            let completion = completions.get(selected)?;\n            Some(self.location.with_path(completion.1.clone()).normalize())\n        }\n    }\n\n    pub fn select(&mut self, forwards: bool) {\n        if let Some(completions) = &self.completions {\n            if completions.is_empty() {\n                self.selected = None;\n            } else {\n                let mut selected = if forwards {\n                    self.selected.and_then(|x| x.checked_add(1)).unwrap_or(0)\n                } else {\n                    self.selected\n                        .and_then(|x| x.checked_sub(1))\n                        .unwrap_or(completions.len() - 1)\n                };\n                if selected >= completions.len() {\n                    selected = 0;\n                }\n                self.selected = Some(selected);\n\n                // Automatically resolve if there is only one completion\n                if completions.len() == 1\n                    && let Some(resolved) = self.resolve()\n                {\n                    self.location = resolved;\n                    self.selected = None;\n                }\n            }\n        } else {\n            self.selected = None;\n        }\n    }\n}\n\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum SearchLocation {\n    Path(PathBuf),\n    Recents,\n    Trash,\n}\n\nimpl std::fmt::Display for SearchLocation {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Path(path) => write!(f, \"{}\", path.display()),\n            Self::Recents => write!(f, \"recents\"),\n            Self::Trash => write!(f, \"trash\"),\n        }\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum SearchItem {\n    Path(PathBuf, String, fs::Metadata),\n    Trash(TrashItem, TrashItemMetadata),\n}\n\nimpl From<Location> for EditLocation {\n    fn from(location: Location) -> Self {\n        Self {\n            location,\n            completions: None,\n            selected: None,\n        }\n    }\n}\n\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum Location {\n    Desktop(PathBuf, String, DesktopConfig),\n    Network(String, String, Option<PathBuf>),\n    Path(PathBuf),\n    Recents,\n    Search(SearchLocation, String, bool, Instant),\n    Trash,\n}\n\nimpl std::fmt::Display for Location {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Desktop(path, display, ..) => {\n                write!(f, \"{} on display {display}\", path.display())\n            }\n            Self::Network(uri, ..) => write!(f, \"{uri}\"),\n            Self::Path(path) => write!(f, \"{}\", path.display()),\n            Self::Recents => write!(f, \"recents\"),\n            Self::Search(location, term, ..) => {\n                write!(f, \"search {} for {}\", location, term)\n            }\n            Self::Trash => write!(f, \"trash\"),\n        }\n    }\n}\n\nimpl Location {\n    pub fn normalize(&self) -> Self {\n        if let Location::Network(uri, ..) = self {\n            if !uri.ends_with('/') {\n                let mut uri = uri.clone();\n                uri.push('/');\n                self.with_uri(uri)\n            } else {\n                self.clone()\n            }\n        } else if let Some(mut path) = self.path_opt().cloned() {\n            // Canonicalize path, if possible\n            if let Ok(canonical) = fs::canonicalize(&path) {\n                path = canonical;\n            }\n            // Add trailing slash if location is a directory\n            if path.is_dir() {\n                path.push(\"\");\n            }\n            self.with_path(path)\n        } else {\n            self.clone()\n        }\n    }\n\n    pub fn ancestors(&self) -> Vec<(Self, String)> {\n        self.path_opt().map_or_else(Default::default, |path| {\n            path.ancestors()\n                .scan(false, |found_home, ancestor| {\n                    (!*found_home).then(|| {\n                        let (name, is_home) = folder_name(ancestor);\n                        *found_home = is_home;\n                        (self.with_path(ancestor.to_path_buf()), name)\n                    })\n                })\n                .collect()\n        })\n    }\n\n    pub const fn path_opt(&self) -> Option<&PathBuf> {\n        match self {\n            Self::Desktop(path, ..) => Some(path),\n            Self::Path(path) => Some(path),\n            Self::Search(SearchLocation::Path(path), ..) => Some(path),\n            Self::Network(_, _, path) => path.as_ref(),\n            _ => None,\n        }\n    }\n\n    pub(crate) fn into_path_opt(self) -> Option<PathBuf> {\n        match self {\n            Self::Desktop(path, ..) => Some(path),\n            Self::Path(path) => Some(path),\n            Self::Search(SearchLocation::Path(path), ..) => Some(path),\n            Self::Network(_, _, path) => path,\n            _ => None,\n        }\n    }\n\n    pub fn with_path(&self, path: PathBuf) -> Self {\n        let path = Self::expand_tilde(path);\n        match self {\n            Self::Desktop(_, display, desktop_config) => {\n                Self::Desktop(path, display.clone(), *desktop_config)\n            }\n            Self::Path(..) => Self::Path(path),\n            Self::Search(SearchLocation::Path(_), term, show_hidden, time) => Self::Search(\n                SearchLocation::Path(path),\n                term.clone(),\n                *show_hidden,\n                *time,\n            ),\n\n            other => other.clone(),\n        }\n    }\n\n    pub fn with_uri(&self, uri: String) -> Self {\n        if let Self::Network(_, name, path) = self {\n            Self::Network(uri, name.clone(), path.clone())\n        } else {\n            self.clone()\n        }\n    }\n\n    pub fn scan(&self, sizes: IconSizes) -> (Option<Box<Item>>, Vec<Item>) {\n        let items = match self {\n            Self::Desktop(path, display, desktop_config) => {\n                scan_desktop(path, display, *desktop_config, sizes)\n            }\n            Self::Path(path) => scan_path(path, sizes),\n            Self::Search(..) => {\n                // Search is done incrementally\n                Vec::new()\n            }\n            Self::Trash => Trash::scan(sizes),\n            Self::Recents => scan_recents(sizes),\n            Self::Network(uri, _, _) => scan_network(uri, sizes),\n        };\n        let parent_item_opt = match self.path_opt() {\n            Some(path) => match item_from_path(path, sizes) {\n                Ok(item) => Some(Box::new(item)),\n                Err(err) => {\n                    log::warn!(\"failed to get item for {}: {}\", path.display(), err);\n                    None\n                }\n            },\n            //TODO: support other locations?\n            None => None,\n        };\n        (parent_item_opt, items)\n    }\n\n    pub fn title(&self) -> String {\n        match self {\n            Self::Desktop(path, _, _) => {\n                let (name, _) = folder_name(path);\n                name\n            }\n            Self::Path(path) => {\n                let (name, _) = folder_name(path);\n                name\n            }\n            Self::Search(location, term, ..) => {\n                let name = match location {\n                    SearchLocation::Path(path) => folder_name(path).0,\n                    SearchLocation::Trash => fl!(\"trash\"),\n                    SearchLocation::Recents => fl!(\"recents\"),\n                };\n\n                //TODO: translate\n                format!(\"Search \\\"{term}\\\": {name}\")\n            }\n            Self::Trash => {\n                fl!(\"trash\")\n            }\n            Self::Recents => {\n                fl!(\"recents\")\n            }\n            Self::Network(display_name, ..) => display_name.clone(),\n        }\n    }\n\n    /// Expand a path that starts with \"~\" with the\n    /// user's home directory\n    pub fn expand_tilde(path: PathBuf) -> PathBuf {\n        let mut components = path.components();\n        match components.next() {\n            Some(path::Component::Normal(os_str)) if os_str == \"~\" => {\n                if let Some(home) = dirs::home_dir() {\n                    home.join(components.as_path())\n                } else {\n                    path\n                }\n            }\n            _ => path,\n        }\n    }\n\n    pub fn is_trash(&self) -> bool {\n        matches!(\n            self,\n            Location::Trash | Location::Search(SearchLocation::Trash, ..)\n        )\n    }\n\n    pub fn is_recents(&self) -> bool {\n        matches!(\n            self,\n            Location::Recents | Location::Search(SearchLocation::Recents, ..)\n        )\n    }\n\n    /// Returns true if this location supports paste operations (not Trash)\n    pub fn supports_paste(&self) -> bool {\n        matches!(\n            self,\n            Self::Desktop(..)\n                | Self::Path(..)\n                | Self::Search(..)\n                | Self::Recents\n                | Self::Network(_, _, Some(_))\n        )\n    }\n}\n\npub struct TaskWrapper(pub cosmic::Task<Message>);\n\nimpl From<cosmic::Task<Message>> for TaskWrapper {\n    fn from(task: cosmic::Task<Message>) -> Self {\n        Self(task)\n    }\n}\n\nimpl fmt::Debug for TaskWrapper {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"TaskWrapper\").finish()\n    }\n}\n\n#[derive(Debug)]\npub enum Command {\n    Action(Action),\n    AddNetworkDrive,\n    AddToSidebar(PathBuf),\n    AutoScroll(Option<f32>),\n    ChangeLocation(String, Location, Option<Vec<PathBuf>>),\n    ContextMenu(Option<Point>, Option<window::Id>),\n    Delete(Vec<PathBuf>),\n    DropFiles(PathBuf, ClipboardPaste),\n    EmptyTrash,\n    #[cfg(feature = \"desktop\")]\n    ExecEntryAction(cosmic::desktop::DesktopEntryData, usize),\n    Iced(TaskWrapper),\n    OpenFile(Vec<PathBuf>),\n    OpenInNewTab(PathBuf),\n    OpenInNewWindow(PathBuf),\n    OpenTrash,\n    Preview(PreviewKind),\n    RunContextAction(usize),\n    SetOpenWith(Mime, String),\n    SetPermissions(PathBuf, u32),\n    SetMultiplePermissions(Vec<(PathBuf, u32)>),\n    SetSort(String, HeadingOptions, bool),\n    WindowDrag,\n    WindowToggleMaximize,\n}\n\n#[derive(Clone, Debug)]\npub enum Message {\n    AddNetworkDrive,\n    AutoScroll(Option<f32>),\n    Click(Option<usize>),\n    DoubleClick(Option<usize>),\n    ClickRelease(Option<usize>),\n    Config(TabConfig),\n    ContextAction(Action),\n    ContextMenu(Option<Point>, Option<window::Id>),\n    LocationContextMenuPoint(Option<Point>),\n    LocationContextMenuIndex(Option<Point>, Option<usize>),\n    LocationMenuAction(LocationMenuAction),\n    Drag(Option<Rectangle>),\n    DragEnd,\n    EditLocation(Option<EditLocation>),\n    EditLocationComplete(usize),\n    EditLocationEnable,\n    EditLocationSubmit,\n    EditLocationTab,\n    OpenInNewTab(PathBuf),\n    EmptyTrash,\n    #[cfg(feature = \"desktop\")]\n    ExecEntryAction(Option<PathBuf>, usize),\n    Gallery(bool),\n    GalleryPrevious,\n    GalleryNext,\n    GalleryToggle,\n    GoNext,\n    GoPrevious,\n    ItemDown,\n    ItemLeft,\n    ItemRight,\n    ItemUp,\n    Location(Location),\n    LocationUp,\n    Open(Option<PathBuf>),\n    Reload,\n    RightClick(Option<Point>, Option<usize>),\n    MiddleClick(usize),\n    Resize(Rectangle),\n    Scroll(Viewport),\n    ScrollTab(f32),\n    ScrollToFocused,\n    SearchContext(Location, SearchContextWrapper),\n    SearchReady(bool),\n    SelectAll,\n    SelectFirst,\n    SelectLast,\n    SetOpenWith(Mime, String),\n    RunContextAction(usize),\n    SetPermissions(PathBuf, u32),\n    ShiftPermissions(Option<(PathBuf, u32)>, u32, u32),\n    SetSort(HeadingOptions, bool),\n    TabComplete(PathBuf, Vec<(String, PathBuf)>),\n    Thumbnail(PathBuf, ItemThumbnail),\n    ToggleSort(HeadingOptions),\n    Drop(Option<(Location, ClipboardPaste)>),\n    DndHover(Location),\n    DndEnter(Location),\n    DndLeave(Location),\n    WindowDrag,\n    WindowToggleMaximize,\n    ZoomIn,\n    ZoomOut,\n    HighlightDeactivate(usize),\n    HighlightActivate(usize),\n    DirectorySize(PathBuf, DirSize),\n    ImageDecoded(PathBuf, u32, u32, Vec<u8>, Option<(u32, u32)>, u64), // path, width, height, pixels, display_size, generation\n}\n\n#[derive(Copy, Clone, Debug, Eq, PartialEq)]\npub enum LocationMenuAction {\n    OpenInNewTab(usize),\n    OpenInNewWindow(usize),\n    Preview(usize),\n    AddToSidebar(usize),\n}\n\nimpl MenuAction for LocationMenuAction {\n    type Message = Message;\n\n    fn message(&self) -> Self::Message {\n        Message::LocationMenuAction(*self)\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum DirSize {\n    Calculating(Controller),\n    Directory(u64),\n    NotDirectory,\n    Error(String),\n}\n\n#[derive(Clone, Debug)]\npub enum ItemMetadata {\n    Path {\n        metadata: Metadata,\n        children_opt: Option<usize>,\n    },\n    Trash {\n        metadata: trash::TrashItemMetadata,\n        entry: trash::TrashItem,\n    },\n    SimpleDir {\n        entries: u64,\n    },\n    SimpleFile {\n        size: u64,\n    },\n    #[cfg(feature = \"gvfs\")]\n    GvfsPath {\n        mtime: u64,\n        size_opt: Option<u64>,\n        children_opt: Option<usize>,\n    },\n}\n\nimpl ItemMetadata {\n    pub fn is_dir(&self) -> bool {\n        match self {\n            Self::Path { metadata, .. } => metadata.is_dir(),\n            Self::Trash { metadata, .. } => match metadata.size {\n                trash::TrashItemSize::Entries(_) => true,\n                trash::TrashItemSize::Bytes(_) => false,\n            },\n            Self::SimpleDir { .. } => true,\n            Self::SimpleFile { .. } => false,\n            #[cfg(feature = \"gvfs\")]\n            Self::GvfsPath { children_opt, .. } => children_opt.is_some(),\n        }\n    }\n\n    pub fn modified(&self) -> Option<SystemTime> {\n        match self {\n            Self::Path { metadata, .. } => metadata.modified().ok(),\n            #[cfg(feature = \"gvfs\")]\n            Self::GvfsPath { mtime, .. } => {\n                Some(SystemTime::UNIX_EPOCH + Duration::from_secs(*mtime))\n            }\n            _ => None,\n        }\n    }\n\n    pub fn file_size(&self) -> Option<u64> {\n        match self {\n            Self::Path { metadata, .. } => (!metadata.is_dir()).then_some(metadata.len()),\n            Self::Trash { metadata, .. } => match metadata.size {\n                TrashItemSize::Bytes(size) => Some(size),\n                TrashItemSize::Entries(_) => None,\n            },\n            #[cfg(feature = \"gvfs\")]\n            Self::GvfsPath { size_opt, .. } => *size_opt,\n            _ => None,\n        }\n    }\n\n    pub fn children_count(&self) -> Option<&usize> {\n        match &self {\n            ItemMetadata::Path { children_opt, .. } => children_opt.as_ref(),\n            #[cfg(feature = \"gvfs\")]\n            ItemMetadata::GvfsPath { children_opt, .. } => children_opt.as_ref(),\n            _ => None,\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum ItemThumbnail {\n    NotImage,\n    Image(widget::image::Handle, Option<(u32, u32)>),\n    Svg(widget::svg::Handle),\n    Text(widget::text_editor::Content),\n}\n\nimpl Clone for ItemThumbnail {\n    fn clone(&self) -> Self {\n        match self {\n            Self::NotImage => Self::NotImage,\n            Self::Image(handle, size_opt) => Self::Image(handle.clone(), *size_opt),\n            Self::Svg(handle) => Self::Svg(handle.clone()),\n            // Content cannot be cloned simply\n            Self::Text(content) => {\n                Self::Text(widget::text_editor::Content::with_text(&content.text()))\n            }\n        }\n    }\n}\n\nimpl ItemThumbnail {\n    pub fn new(\n        path: &Path,\n        metadata: ItemMetadata,\n        mime: mime::Mime,\n        mut thumbnail_size: u32,\n        max_mem: u64,\n        jobs: usize,\n        max_size_mb: u64,\n    ) -> Self {\n        let thumbnail_cacher =\n            ThumbnailCacher::new(path, ThumbnailSize::from_pixel_size(thumbnail_size));\n        match thumbnail_cacher.as_ref() {\n            Ok(cache) => match cache.get_cached_thumbnail() {\n                CachedThumbnail::Valid((thumbnail_path, size)) => {\n                    // Check original image dimensions even when loading cached thumbnail\n                    // This prevents trying to load huge images in preview mode\n                    let original_dims = match image::image_dimensions(path) {\n                        Ok((width, height)) => Some((width, height)),\n                        Err(_) => size.map(|s| (s.pixel_size(), s.pixel_size())),\n                    };\n\n                    return Self::Image(\n                        widget::image::Handle::from_path(thumbnail_path),\n                        original_dims,\n                    );\n                }\n                CachedThumbnail::Failed => {\n                    if mime.type_() != mime::IMAGE {\n                        return Self::NotImage;\n                    }\n                }\n                CachedThumbnail::RequiresUpdate(size) => {\n                    thumbnail_size = size.pixel_size();\n                }\n            },\n            Err(err) => {\n                log::warn!(\n                    \"failed to create ThumbnailCache for {}: {}\",\n                    path.display(),\n                    err\n                );\n            }\n        }\n\n        let size = metadata.file_size().unwrap_or_default();\n        let check_size = |thumbnailer: &str, max_size| {\n            if size <= max_size {\n                true\n            } else {\n                log::warn!(\n                    \"skipping internal {} thumbnailer for {}: file size {} is larger than {}\",\n                    thumbnailer,\n                    path.display(),\n                    format_size(size),\n                    format_size(max_size)\n                );\n                false\n            }\n        };\n\n        let mut tried_supported_file = false;\n        // First try built-in image thumbnailer\n        if mime.type_() == mime::IMAGE && check_size(\"image\", max_size_mb * 1000 * 1000) {\n            // Check if image dimensions would exceed available memory budget\n            // The GPU tiling system can handle large images, but we still need to decode them first\n            let dimensions_ok = match image::image_dimensions(path) {\n                Ok((width, height)) => {\n                    if exceeds_memory_limit(width, height, max_mem) {\n                        log::warn!(\n                            \"skipping thumbnail generation for {}: {}x{} image would exceed {}MB memory budget\",\n                            path.display(),\n                            width,\n                            height,\n                            max_mem\n                        );\n                        false\n                    } else {\n                        if should_use_tiling(width, height) {\n                            log::info!(\n                                \"Large image {}x{} detected, will use GPU tiling for display\",\n                                width,\n                                height\n                            );\n                        }\n                        true\n                    }\n                }\n                Err(err) => {\n                    log::debug!(\n                        \"failed to read dimensions for {}: {}, will try decoding\",\n                        path.display(),\n                        err\n                    );\n                    true // If we can't read dimensions, try anyway\n                }\n            };\n\n            if !dimensions_ok {\n                // Skip this image entirely since it is too large to safely decode\n                return Self::NotImage;\n            }\n\n            tried_supported_file = true;\n            let dyn_img = match image::ImageReader::open(path)\n                .and_then(image::ImageReader::with_guessed_format)\n            {\n                Ok(mut reader) => {\n                    let mut limits = image::Limits::default();\n                    let max_ram = max_mem * 1000 * 1000 / jobs as u64;\n                    limits.max_alloc = Some(max_ram);\n                    reader.limits(limits);\n                    match reader.decode() {\n                        Ok(reader) => Some(reader),\n                        Err(err) => {\n                            log::warn!(\"failed to decode {}: {}\", path.display(), err);\n                            None\n                        }\n                    }\n                }\n                Err(err) => {\n                    log::warn!(\"failed to read {}: {}\", path.display(), err);\n                    None\n                }\n            };\n\n            if let Some(dyn_img) = dyn_img {\n                let (img_width, img_height) = (dyn_img.width(), dyn_img.height());\n\n                if let Ok(cacher) = thumbnail_cacher.as_ref() {\n                    match cacher.update_with_image(dyn_img) {\n                        Ok(thumb_path) => {\n                            return Self::Image(\n                                widget::image::Handle::from_path(thumb_path),\n                                Some((img_width, img_height)),\n                            );\n                        }\n                        Err(err) => {\n                            log::warn!(\"cacher failed to decode {}: {}\", path.display(), err);\n                        }\n                    }\n                } else {\n                    // Fallback for when thumbnail cacher isn't available.\n                    let thumbnail = dyn_img\n                        .thumbnail(thumbnail_size, thumbnail_size)\n                        .into_rgba8();\n                    return Self::Image(\n                        widget::image::Handle::from_rgba(\n                            thumbnail.width(),\n                            thumbnail.height(),\n                            thumbnail.into_raw(),\n                        ),\n                        Some((img_width, img_height)),\n                    );\n                }\n            }\n        }\n\n        // Try external thumbnailers.\n        let thumbnail_dir = thumbnail_cacher\n            .as_ref()\n            .ok()\n            .map(ThumbnailCacher::thumbnail_dir);\n        if let Some((item_thumbnail, temp_file)) =\n            Self::generate_thumbnail_external(path, &mime, thumbnail_size, thumbnail_dir)\n        {\n            if let Ok(cache) = thumbnail_cacher\n                && let Err(err) = cache.update_with_temp_file(temp_file)\n            {\n                log::warn!(\"failed to update cache for {}: {}\", path.display(), err);\n            }\n            return item_thumbnail;\n        }\n\n        tried_supported_file = tried_supported_file || !thumbnailer(&mime).is_empty();\n\n        // Try internal thumbnailers that don't get cached.\n        //TODO: adjust limits for internal thumbnailers as desired\n        if mime.type_() == mime::IMAGE\n            && mime.subtype() == mime::SVG\n            && check_size(\"svg\", 8 * 1000 * 1000)\n        {\n            tried_supported_file = true;\n            // Try built-in svg thumbnailer\n            match fs::read(path) {\n                Ok(data) => {\n                    //TODO: validate SVG data\n                    return Self::Svg(widget::svg::Handle::from_memory(data));\n                }\n                Err(err) => {\n                    log::warn!(\"failed to read {}: {}\", path.display(), err);\n                }\n            }\n        } else if mime.type_() == mime::TEXT && check_size(\"text\", 8 * 1000 * 1000) {\n            /*TODO: fix performance issues, widget::text_editor::Content::with_text forces all text to shape, which blocks rendering\n            match fs::read_to_string(&path) {\n                Ok(data) => {\n                    return ItemThumbnail::Text(widget::text_editor::Content::with_text(&data));\n                }\n                Err(err) => {\n                    log::warn!(\"failed to read {}: {}\", path.display(), err);\n                }\n            }\n            */\n        }\n\n        // If we weren't able to create a thumbnail, but we should have\n        // been able to, create a fail marker so that it isn't tried the\n        // next time.\n        if let Ok(cacher) = thumbnail_cacher\n            && tried_supported_file\n            && let Err(err) = cacher.create_fail_marker()\n        {\n            log::warn!(\n                \"failed to create thumbnail fail marker for {}: {}\",\n                path.display(),\n                err\n            );\n        }\n\n        Self::NotImage\n    }\n\n    fn generate_thumbnail_external(\n        path: &Path,\n        mime: &mime::Mime,\n        thumbnail_size: u32,\n        thumbnail_dir: Option<&Path>,\n    ) -> Option<(Self, NamedTempFile)> {\n        // Try external thumbnailers\n        for thumbnailer in thumbnailer(mime) {\n            let is_evince = thumbnailer.exec.starts_with(\"evince-thumbnailer \");\n            let prefix = if is_evince {\n                //TODO: apparmor config for evince-thumbnailer does not allow /tmp/cosmic-files*\n                \"gnome-desktop-\"\n            } else {\n                \"cosmic-files-\"\n            };\n\n            // It's preferable to create the tempfile in the same directory as the final cached\n            // thumbnail to ensure that no copies across filesytems need to be made. However,\n            // the apparmor config for evince-thumbnailer does not allow this, so we need to\n            // fallback to the system tempdir.\n            let dir = if is_evince { None } else { thumbnail_dir };\n            let file = match dir {\n                Some(d) => tempfile::Builder::new().prefix(prefix).tempfile_in(d),\n                None => tempfile::Builder::new().prefix(prefix).tempfile(),\n            };\n            let file = match file {\n                Ok(ok) => ok,\n                Err(err) => {\n                    log::warn!(\n                        \"failed to create temporary file for thumbnail of {}: {}\",\n                        path.display(),\n                        err\n                    );\n                    continue;\n                }\n            };\n\n            let Some(mut command) = thumbnailer.command(path, file.path(), thumbnail_size) else {\n                continue;\n            };\n            match command.status() {\n                Ok(status) => {\n                    if status.success() {\n                        match image::ImageReader::open(file.path())\n                            .and_then(ImageReader::with_guessed_format)\n                        {\n                            Ok(reader) => match reader.decode().map(DynamicImage::into_rgba8) {\n                                Ok(image) => {\n                                    return Some((\n                                        Self::Image(\n                                            widget::image::Handle::from_rgba(\n                                                image.width(),\n                                                image.height(),\n                                                image.into_raw(),\n                                            ),\n                                            None,\n                                        ),\n                                        file,\n                                    ));\n                                }\n                                Err(err) => {\n                                    log::warn!(\"failed to decode {}: {}\", path.display(), err);\n                                }\n                            },\n                            Err(err) => {\n                                log::warn!(\"failed to read {}: {}\", path.display(), err);\n                            }\n                        }\n                    } else {\n                        log::warn!(\n                            \"failed to run {:?} for {}: {}\",\n                            thumbnailer,\n                            path.display(),\n                            status\n                        );\n                    }\n                }\n                Err(err) => {\n                    log::warn!(\n                        \"failed to run {thumbnailer:?} for {}: {}\",\n                        path.display(),\n                        err\n                    );\n                }\n            }\n        }\n\n        None\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct Item {\n    pub name: String,\n    pub is_mount_point: bool,\n    pub display_name: String,\n    pub metadata: ItemMetadata,\n    pub hidden: bool,\n    pub location_opt: Option<Location>,\n    pub mime: Mime,\n    pub image_dimensions: Option<(u32, u32)>,\n    pub icon_handle_grid: widget::icon::Handle,\n    pub icon_handle_list: widget::icon::Handle,\n    pub icon_handle_list_condensed: widget::icon::Handle,\n    pub thumbnail_opt: Option<ItemThumbnail>,\n    pub button_id: widget::Id,\n    pub pos_opt: Cell<Option<(usize, usize)>>,\n    pub rect_opt: Cell<Option<Rectangle>>,\n    pub selected: bool,\n    pub highlighted: bool,\n    pub cut: bool,\n    pub overlaps_drag_rect: bool,\n    pub dir_size: DirSize,\n}\n\nimpl Item {\n    fn display_name(name: &str) -> String {\n        // In order to wrap at periods and underscores, add a zero width space after each one\n        name.replace('.', \".\\u{200B}\").replace('_', \"_\\u{200B}\")\n    }\n\n    /// Text widget for a filename in grid/icon view: word-or-glyph wrapping, middle-ellipsized to 3 lines.\n    fn grid_display_name<'a>(\n        name: impl Into<Cow<'a, str>> + 'a,\n    ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {\n        widget::text::body(name)\n            .wrapping(text::Wrapping::WordOrGlyph)\n            .ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(\n                3,\n            )))\n    }\n\n    /// Text widget for a filename in list view: word-or-glyph wrapping, middle-ellipsized to 1 line.\n    fn list_display_name<'a>(\n        name: impl Into<Cow<'a, str>> + 'a,\n    ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {\n        widget::text::body(name)\n            .wrapping(text::Wrapping::WordOrGlyph)\n            .ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(\n                1,\n            )))\n    }\n\n    pub fn path_opt(&self) -> Option<&PathBuf> {\n        self.location_opt.as_ref()?.path_opt()\n    }\n\n    pub fn can_gallery(&self) -> bool {\n        self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT\n    }\n\n    pub fn file_metadata(&self) -> Option<Metadata> {\n        match &self.metadata {\n            ItemMetadata::Path { metadata, .. } => Some(metadata.clone()),\n            #[cfg(feature = \"gvfs\")]\n            ItemMetadata::GvfsPath { .. } => self.path_opt().and_then(|p| fs::metadata(p).ok()),\n            _ => {\n                //TODO: other metadata types\n                None\n            }\n        }\n    }\n\n    fn preview(&self) -> Element<'_, Message> {\n        let spacing = cosmic::theme::spacing();\n        // This loads the image only if thumbnailing worked\n        let icon = widget::icon::icon(self.icon_handle_grid.clone())\n            .content_fit(ContentFit::Contain)\n            .size(IconSizes::default().grid())\n            .into();\n        match self\n            .thumbnail_opt\n            .as_ref()\n            .unwrap_or(&ItemThumbnail::NotImage)\n        {\n            ItemThumbnail::NotImage => icon,\n            ItemThumbnail::Image(handle, _original_dims) => {\n                // Preview pane: ALWAYS show thumbnail for instant, responsive UI\n                // Full resolution loading happens in gallery mode\n                widget::image(handle.clone()).into()\n            }\n            ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(),\n            ItemThumbnail::Text(content) => widget::text_editor(content)\n                .class(cosmic::theme::iced::TextEditor::Custom(Box::new(\n                    text_editor_class,\n                )))\n                .width(THUMBNAIL_SIZE as f32)\n                .height(Length::Fixed(THUMBNAIL_SIZE as f32))\n                .padding(spacing.space_xxs)\n                .into(),\n        }\n    }\n\n    pub fn preview_actions(&self) -> Element<'_, Message> {\n        let mut row = widget::row::with_capacity(3)\n            .align_y(Alignment::Center)\n            .spacing(theme::spacing().space_xxs)\n            .push(\n                widget::button::icon(widget::icon::from_name(\"go-previous-symbolic\"))\n                    .on_press(Message::ItemLeft),\n            )\n            .push(\n                widget::button::icon(widget::icon::from_name(\"go-next-symbolic\"))\n                    .on_press(Message::ItemRight),\n            );\n        if self.can_gallery()\n            && let Some(_path) = self.path_opt()\n        {\n            row = row.push(\n                widget::button::icon(widget::icon::from_name(\"view-fullscreen-symbolic\"))\n                    .on_press(Message::Gallery(true)),\n            );\n        }\n        row.into()\n    }\n\n    pub fn preview_view<'a>(\n        &'a self,\n        mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>,\n        military_time: bool,\n    ) -> Element<'a, Message> {\n        let cosmic_theme::Spacing {\n            space_xxxs,\n            space_m,\n            ..\n        } = theme::spacing();\n\n        let mut column = widget::column::with_capacity(4).spacing(space_m);\n\n        column = column.push(\n            widget::container(self.preview())\n                .center_x(Length::Fill)\n                .max_height(THUMBNAIL_SIZE as f32),\n        );\n\n        let mut details = widget::column::with_capacity(8).spacing(space_xxxs);\n        details = details.push(widget::text::heading(self.name.clone()));\n        details = details.push(widget::text::body(fl!(\n            \"type\",\n            mime = self.mime.to_string()\n        )));\n        let mut settings = Vec::new();\n        if let Some(mime_app_cache) = mime_app_cache_opt {\n            let mime_apps = mime_app_cache.get(&self.mime);\n            if !mime_apps.is_empty() {\n                settings.push(\n                    widget::settings::item::builder(fl!(\"open-with\")).control(\n                        Element::from(\n                            widget::dropdown(\n                                mime_apps,\n                                mime_apps.iter().position(|x| x.is_default),\n                                move |index| index,\n                            )\n                            .icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))),\n                        )\n                        .map(|index| {\n                            let mime_app = &mime_apps[index];\n                            Message::SetOpenWith(self.mime.clone(), mime_app.id.clone())\n                        }),\n                    ),\n                );\n            }\n        }\n\n        if let Some(metadata) = self.file_metadata() {\n            if metadata.is_dir() {\n                if let Some(children) = self.metadata.children_count() {\n                    details = details.push(widget::text::body(fl!(\"items\", items = children)));\n                }\n                let size = match &self.dir_size {\n                    DirSize::Calculating(_) => fl!(\"calculating\"),\n                    DirSize::Directory(size) => format_size(*size),\n                    DirSize::NotDirectory => String::new(),\n                    DirSize::Error(err) => err.clone(),\n                };\n                if !size.is_empty() {\n                    details = details.push(widget::text::body(fl!(\"item-size\", size = size)));\n                }\n            } else {\n                details = details.push(widget::text::body(fl!(\n                    \"item-size\",\n                    size = format_size(metadata.len())\n                )));\n            }\n\n            let date_time_formatter = date_time_formatter(military_time);\n            let time_formatter = time_formatter(military_time);\n\n            if let Ok(time) = metadata.created() {\n                details = details.push(widget::text::body(fl!(\n                    \"item-created\",\n                    created = format_time(time, &date_time_formatter, &time_formatter).to_string()\n                )));\n            }\n\n            if let Ok(time) = metadata.modified() {\n                details = details.push(widget::text::body(fl!(\n                    \"item-modified\",\n                    modified = format_time(time, &date_time_formatter, &time_formatter).to_string()\n                )));\n            }\n\n            if let Ok(time) = metadata.accessed() {\n                details = details.push(widget::text::body(fl!(\n                    \"item-accessed\",\n                    accessed = format_time(time, &date_time_formatter, &time_formatter).to_string()\n                )));\n            }\n\n            #[cfg(unix)]\n            if let Some(path) = self.path_opt() {\n                use std::os::unix::fs::MetadataExt;\n\n                let mode = metadata.mode();\n\n                let user_name = uzers::get_user_by_uid(metadata.uid())\n                    .and_then(|user| user.name().to_str().map(ToOwned::to_owned))\n                    .unwrap_or_default();\n                let user_path = path.clone();\n                settings.push(\n                    widget::settings::item::builder(user_name)\n                        .description(fl!(\"owner\"))\n                        .control(widget::dropdown(\n                            Cow::Borrowed(MODE_NAMES.as_slice()),\n                            Some(get_mode_part(mode, MODE_SHIFT_USER).try_into().unwrap()),\n                            move |selected| {\n                                Message::SetPermissions(\n                                    user_path.clone(),\n                                    set_mode_part(\n                                        mode,\n                                        MODE_SHIFT_USER,\n                                        selected.try_into().unwrap(),\n                                    ),\n                                )\n                            },\n                        )),\n                );\n\n                let group_name = uzers::get_group_by_gid(metadata.gid())\n                    .and_then(|group| group.name().to_str().map(ToOwned::to_owned))\n                    .unwrap_or_default();\n                let group_path = path.clone();\n                settings.push(\n                    widget::settings::item::builder(group_name)\n                        .description(fl!(\"group\"))\n                        .control(widget::dropdown(\n                            Cow::Borrowed(MODE_NAMES.as_slice()),\n                            Some(get_mode_part(mode, MODE_SHIFT_GROUP).try_into().unwrap()),\n                            move |selected| {\n                                Message::SetPermissions(\n                                    group_path.clone(),\n                                    set_mode_part(\n                                        mode,\n                                        MODE_SHIFT_GROUP,\n                                        selected.try_into().unwrap(),\n                                    ),\n                                )\n                            },\n                        )),\n                );\n\n                let other_path = path.clone();\n                settings.push(widget::settings::item::builder(fl!(\"other\")).control(\n                    widget::dropdown(\n                        Cow::Borrowed(MODE_NAMES.as_slice()),\n                        Some(get_mode_part(mode, MODE_SHIFT_OTHER).try_into().unwrap()),\n                        move |selected| {\n                            Message::SetPermissions(\n                                other_path.clone(),\n                                set_mode_part(mode, MODE_SHIFT_OTHER, selected.try_into().unwrap()),\n                            )\n                        },\n                    ),\n                ));\n            }\n        }\n\n        if let Some(path) = self.path_opt()\n            && let Ok(img) = image::image_dimensions(path)\n        {\n            let (width, height) = img;\n            details = details.push(widget::text::body(format!(\"{width}x{height}\")));\n        }\n        column = column.push(details);\n\n        if let Some(path) = self.path_opt()\n            && self.selected\n        {\n            column = column.push(\n                widget::button::standard(fl!(\"open\")).on_press(Message::Open(Some(path.clone()))),\n            );\n        }\n\n        if !settings.is_empty() {\n            let mut section = widget::settings::section();\n            section = section.extend(settings);\n            column = column.push(section);\n        }\n\n        column.into()\n    }\n\n    pub fn replace_view(&self, heading: String, military_time: bool) -> Element<'_, Message> {\n        let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing();\n\n        let mut row = widget::row::with_capacity(2).spacing(space_xxxs);\n        row = row.push(self.preview());\n\n        let mut column = widget::column::with_capacity(3).spacing(space_xxxs);\n        column = column.push(widget::text::heading(heading));\n\n        //TODO: translate!\n        //TODO: correct display of folder size?\n        if let ItemMetadata::Path {\n            metadata,\n            children_opt,\n        } = &self.metadata\n        {\n            if metadata.is_dir() {\n                if let Some(children) = children_opt {\n                    column = column.push(widget::text::body(format!(\"Items: {children}\")));\n                }\n            } else {\n                column = column.push(widget::text::body(format!(\n                    \"Size: {}\",\n                    format_size(metadata.len())\n                )));\n            }\n            if let Ok(time) = metadata.modified() {\n                let date_time_formatter = date_time_formatter(military_time);\n                let time_formatter = time_formatter(military_time);\n\n                column = column.push(widget::text::body(format!(\n                    \"Last modified: {}\",\n                    format_time(time, &date_time_formatter, &time_formatter)\n                )));\n            }\n        } else {\n            //TODO: other metadata\n        }\n\n        row = row.push(column);\n        row.into()\n    }\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]\npub enum View {\n    Grid,\n    List,\n}\n#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]\npub enum HeadingOptions {\n    Name = 0,\n    Modified,\n    Size,\n    TrashedOn,\n}\n\nimpl fmt::Display for HeadingOptions {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Name => write!(f, \"{}\", fl!(\"name\")),\n            Self::Modified => write!(f, \"{}\", fl!(\"modified\")),\n            Self::Size => write!(f, \"{}\", fl!(\"size\")),\n            Self::TrashedOn => write!(f, \"{}\", fl!(\"trashed-on\")),\n        }\n    }\n}\n\nimpl HeadingOptions {\n    pub fn names() -> Vec<String> {\n        vec![\n            Self::Name.to_string(),\n            Self::Modified.to_string(),\n            Self::Size.to_string(),\n            Self::TrashedOn.to_string(),\n        ]\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum Mode {\n    App,\n    Desktop,\n    Dialog(DialogKind),\n}\n\nimpl Mode {\n    /// Whether multiple files can be selected in this mode\n    pub fn multiple(&self) -> bool {\n        match self {\n            Self::App | Self::Desktop => true,\n            Self::Dialog(dialog) => dialog.multiple(),\n        }\n    }\n}\n\nstruct SearchContext {\n    results_rx: mpsc::Receiver<SearchItem>,\n    ready: Arc<atomic::AtomicBool>,\n    last_modified_opt: Arc<RwLock<Option<SystemTime>>>,\n}\n\npub struct SearchContextWrapper(Option<SearchContext>);\n\nimpl Clone for SearchContextWrapper {\n    fn clone(&self) -> Self {\n        Self(None)\n    }\n}\n\nimpl fmt::Debug for SearchContextWrapper {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"SearchContextWrapper\").finish()\n    }\n}\n\n// TODO when creating items, pass <Arc<SelectedItems>> to each item\n// as a drag data, so that when dnd is initiated, they are all included\npub struct Tab {\n    //TODO: make more items private\n    pub location: Location,\n    pub location_ancestors: Vec<(Location, String)>,\n    pub location_title: String,\n    pub location_context_menu_point: Option<Point>,\n    pub location_context_menu_index: Option<usize>,\n    pub context_menu: Option<Point>,\n    pub mode: Mode,\n    pub scroll_opt: Option<AbsoluteOffset>,\n    pub size_opt: Cell<Option<Size>>,\n    pub content_height_opt: Cell<Option<f32>>,\n    pub viewport_opt: Option<Rectangle>,\n    pub item_view_size_opt: Cell<Option<Size>>,\n    pub edit_location: Option<EditLocation>,\n    pub edit_location_id: widget::Id,\n    pub history_i: usize,\n    pub history: Vec<Location>,\n    pub config: TabConfig,\n    pub thumb_config: ThumbCfg,\n    pub sort_name: HeadingOptions,\n    pub sort_direction: bool,\n    pub gallery: bool,\n    pub(crate) parent_item_opt: Option<Box<Item>>,\n    pub(crate) items_opt: Option<Vec<Item>>,\n    pub dnd_hovered: Option<(Location, Instant)>,\n    pub(crate) scrollable_id: widget::Id,\n    select_focus: Option<usize>,\n    select_range: Option<(usize, usize)>,\n    clicked: Option<usize>,\n    selected_clicked: bool,\n    last_right_click: Option<usize>,\n    search_context: Option<SearchContext>,\n    date_time_formatter: DateTimeFormatter<fieldsets::YMDT>,\n    time_formatter: DateTimeFormatter<fieldsets::T>,\n    watch_drag: bool,\n    window_id: Option<window::Id>,\n    large_image_manager: LargeImageManager,\n}\n\nasync fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64, OperationError> {\n    let mut total = 0;\n    for entry_res in WalkDir::new(path) {\n        controller\n            .check()\n            .await\n            .map_err(|s| OperationError::from_state(s, &controller))?;\n\n        //TODO: report more errors?\n        if let Ok(entry) = entry_res\n            && let Ok(metadata) = entry.metadata()\n            && metadata.is_file()\n        {\n            total += metadata.len();\n        }\n\n        // Yield in case this process takes a while.\n        tokio::task::yield_now().await;\n    }\n    Ok(total)\n}\n\nfn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {\n    let path = path.as_ref();\n    let mut found_home = false;\n    let name = match path.file_name() {\n        Some(name) => {\n            if path == crate::home_dir() {\n                found_home = true;\n                fl!(\"home\")\n            } else {\n                match (get_filename_from_path(path), fs::metadata(path)) {\n                    (Ok(name), Ok(metadata)) => {\n                        let is_gvfs = fs_kind(&metadata) == FsKind::Gvfs;\n                        display_name_for_file(path, &name, is_gvfs, false)\n                    }\n                    _ => name.to_string_lossy().into_owned(),\n                }\n            }\n        }\n        None => {\n            fl!(\"filesystem\")\n        }\n    };\n    (name, found_home)\n}\n\n// parse .hidden file and return files path\npub fn parse_hidden_file(path: &PathBuf) -> Box<[String]> {\n    let Ok(file) = File::open(path) else {\n        return Default::default();\n    };\n\n    BufReader::new(file)\n        .lines()\n        .map_while(Result::ok)\n        .filter_map(|line| {\n            let line = line.trim();\n            (!line.is_empty()).then_some(line.to_owned())\n        })\n        .collect()\n}\n\nimpl Tab {\n    pub fn new(\n        location: Location,\n        config: TabConfig,\n        thumb_config: ThumbCfg,\n        sorting_options: Option<&FxOrderMap<String, (HeadingOptions, bool)>>,\n        scrollable_id: widget::Id,\n        window_id: Option<window::Id>,\n    ) -> Self {\n        let location_str = location.to_string();\n        let (sort_name, sort_direction) = sorting_options\n            .and_then(|opts| opts.get(&location_str))\n            .or_else(|| SORT_OPTION_FALLBACK.get(&location_str))\n            .copied()\n            .unwrap_or((HeadingOptions::Name, true));\n        let location = location.normalize();\n        let location_ancestors = location.ancestors();\n        let location_title = location.title();\n        let history = vec![location.clone()];\n        Self {\n            location,\n            location_ancestors,\n            location_title,\n            context_menu: None,\n            location_context_menu_point: None,\n            location_context_menu_index: None,\n            mode: Mode::App,\n            scroll_opt: None,\n            size_opt: Cell::new(None),\n            content_height_opt: Cell::new(None),\n            viewport_opt: None,\n            item_view_size_opt: Cell::new(None),\n            edit_location: None,\n            edit_location_id: widget::Id::unique(),\n            history_i: 0,\n            history,\n            config,\n            thumb_config,\n            sort_name,\n            sort_direction,\n            gallery: false,\n            parent_item_opt: None,\n            items_opt: None,\n            scrollable_id,\n            select_focus: None,\n            select_range: None,\n            clicked: None,\n            dnd_hovered: None,\n            selected_clicked: false,\n            last_right_click: None,\n            search_context: None,\n            date_time_formatter: date_time_formatter(config.military_time),\n            time_formatter: time_formatter(config.military_time),\n            watch_drag: true,\n            window_id,\n            large_image_manager: LargeImageManager::new(),\n        }\n    }\n\n    pub fn title(&self) -> String {\n        //TODO: is it possible to return a &str?\n        self.location_title.clone()\n    }\n\n    pub const fn items_opt(&self) -> Option<&Vec<Item>> {\n        self.items_opt.as_ref()\n    }\n\n    pub const fn items_opt_mut(&mut self) -> Option<&mut Vec<Item>> {\n        self.items_opt.as_mut()\n    }\n\n    pub fn set_items(&mut self, mut items: Vec<Item>) {\n        let highlighted = self\n            .items_opt\n            .as_ref()\n            .and_then(|items| items.iter().enumerate().find(|i| i.1.highlighted))\n            .map(|(i, _)| i);\n        let selected = self.selected_locations();\n        for item in &mut items {\n            item.selected = false;\n            if let Some(location) = &item.location_opt\n                && selected.contains(location)\n            {\n                item.selected = true;\n            }\n        }\n        self.items_opt = Some(items);\n        if let Some(i) = highlighted\n            .zip(self.items_opt.as_mut())\n            .and_then(|(h, items)| items.get_mut(h))\n        {\n            i.highlighted = true;\n        }\n    }\n\n    pub fn cut_selected(&mut self) {\n        if let Some(ref mut items) = self.items_opt {\n            for item in items.iter_mut() {\n                item.cut = item.selected;\n            }\n        }\n    }\n\n    pub fn refresh_cut(&mut self, locations: &[PathBuf]) {\n        if let Some(ref mut items) = self.items_opt {\n            for item in items.iter_mut() {\n                item.cut = false;\n                if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt)\n                    && locations.contains(location_path)\n                {\n                    item.cut = true;\n                }\n            }\n        }\n    }\n\n    pub fn selected_locations(&self) -> Vec<Location> {\n        if let Some(ref items) = self.items_opt {\n            items\n                .iter()\n                .filter_map(|item| {\n                    if item.selected {\n                        item.location_opt.clone()\n                    } else {\n                        None\n                    }\n                })\n                .collect()\n        } else {\n            Vec::new()\n        }\n    }\n\n    pub fn select_all(&mut self) {\n        if let Some(ref mut items) = self.items_opt {\n            for item in items.iter_mut() {\n                if !self.config.show_hidden && item.hidden {\n                    item.selected = false;\n                    continue;\n                }\n                item.selected = true;\n            }\n        }\n    }\n\n    pub fn select_none(&mut self) -> bool {\n        self.select_focus = None;\n        let mut had_selection = false;\n        if let Some(ref mut items) = self.items_opt {\n            for item in items.iter_mut() {\n                if item.selected {\n                    item.selected = false;\n                    had_selection = true;\n                }\n            }\n        }\n        had_selection\n    }\n\n    pub fn select_name(&mut self, name: &str) {\n        self.select_focus = None;\n        if let Some(ref mut items) = self.items_opt {\n            for (i, item) in items.iter_mut().enumerate() {\n                item.selected = item.name == name;\n                if item.selected {\n                    self.select_focus = Some(i);\n                }\n            }\n        }\n    }\n\n    /// Selects the first item whose name starts with the given prefix (case-insensitive).\n    /// Returns true if an item was selected.\n    pub fn select_by_prefix(&mut self, prefix: &str) -> bool {\n        let prefix_lower = prefix.to_lowercase();\n        let focus = self.select_focus.take();\n\n        if let Some(ref mut items) = self.items_opt {\n            // First, deselect all items\n            for item in items.iter_mut() {\n                item.selected = false;\n            }\n\n            // Determine the start index of the search. When the index is before the currently focused item, it will be\n            // considered first, otherwise last. Consider the focused item last when only a single character has been\n            // typed, so we eagerly switch focus on the first character and stay on the same item as long as the prefix\n            // matches.\n            let single_char = prefix_lower.chars().count() == 1;\n            let start = if single_char {\n                Self::index_after_focus(focus, self.sort_direction)\n            } else {\n                Self::index_before_focus(focus, self.sort_direction)\n            };\n            self.select_focus = Self::select_first_prefix_from_index(\n                &prefix_lower,\n                items,\n                start,\n                self.sort_direction,\n            );\n\n            if self.select_focus.is_some() || single_char {\n                return self.select_focus.is_some();\n            }\n\n            let mut chars = prefix_lower.chars();\n            let Some(first) = chars.next() else {\n                log::error!(\"search term is empty\");\n                return self.select_focus.is_some();\n            };\n\n            // Check if all entered characters are the same\n            if !chars.all(|c| c == first) {\n                return self.select_focus.is_some();\n            }\n\n            // Search for a single character when all entered characters are the same.\n            // This allows cycling through items starting with the same character by repeatedly pressing a key.\n            let start = Self::index_after_focus(focus, self.sort_direction);\n            self.select_focus = Self::select_first_prefix_from_index(\n                &first.to_string(),\n                items,\n                start,\n                self.sort_direction,\n            );\n\n            return self.select_focus.is_some();\n        }\n        false\n    }\n\n    fn index_before_focus(current_focus: Option<usize>, forward: bool) -> usize {\n        current_focus.map_or(0, |i| if forward { i } else { i + 1 })\n    }\n\n    fn index_after_focus(current_focus: Option<usize>, forward: bool) -> usize {\n        current_focus.map_or(0, |i| if forward { i + 1 } else { i })\n    }\n\n    fn select_first_prefix_from_index(\n        prefix_lower: &str,\n        items: &mut [Item],\n        start: usize,\n        forward: bool,\n    ) -> Option<usize> {\n        // Order the search item so they begin at `start`.\n        let Some((until, after)) = items.split_at_mut_checked(start) else {\n            log::error!(\n                \"invalid start index {start} for items of length {}\",\n                items.len()\n            );\n            return None;\n        };\n        let search_items = after\n            .iter_mut()\n            .enumerate()\n            .map(|(i, item)| (i + start, item))\n            .chain(until.iter_mut().enumerate());\n\n        if forward {\n            Self::select_first_prefix_match(prefix_lower, search_items)\n        } else {\n            Self::select_first_prefix_match(prefix_lower, search_items.rev())\n        }\n    }\n\n    /// Selects the first item in the given iterator whose name starts with the given prefix.\n    ///\n    /// The `prefix` must be lowercase.\n    fn select_first_prefix_match<'a>(\n        prefix: &str,\n        items: impl Iterator<Item = (usize, &'a mut Item)>,\n    ) -> Option<usize> {\n        for (i, item) in items {\n            if item.name.to_lowercase().starts_with(prefix) {\n                item.selected = true;\n                return Some(i);\n            }\n        }\n        None\n    }\n\n    pub fn select_paths(&mut self, paths: Vec<PathBuf>) {\n        self.select_focus = None;\n        if let Some(ref mut items) = self.items_opt {\n            for (i, item) in items.iter_mut().enumerate() {\n                item.selected = false;\n                if let Some(path) = item.path_opt()\n                    && paths.contains(path)\n                {\n                    item.selected = true;\n                    self.select_focus = Some(i);\n                }\n            }\n        }\n    }\n\n    fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool {\n        let mut start = (row, col);\n        let mut end = (row, col);\n        if mod_shift {\n            if self.select_focus.is_none() || self.select_range.is_none() {\n                // Set select range to initial state if necessary\n                self.select_range = self.select_focus.map(|i| (i, i));\n            }\n\n            if let Some(pos) = self.select_range_start_pos_opt() {\n                if pos.0 < row || (pos.0 == row && pos.1 < col) {\n                    start = pos;\n                } else {\n                    end = pos;\n                }\n            }\n        }\n\n        let mut found = false;\n        if let Some(ref mut items) = self.items_opt {\n            for (i, item) in items.iter_mut().enumerate() {\n                item.selected = false;\n                let pos = match item.pos_opt.get() {\n                    Some(some) => some,\n                    None => continue,\n                };\n                if pos.0 < start.0 || (pos.0 == start.0 && pos.1 < start.1) {\n                    // Before start\n                    continue;\n                }\n                if pos.0 > end.0 || (pos.0 == end.0 && pos.1 > end.1) {\n                    // After end\n                    continue;\n                }\n                if pos == (row, col) {\n                    // Update focus if this is what we wanted to select\n                    self.select_focus = Some(i);\n                    self.select_range = if mod_shift {\n                        self.select_range.map(|r| (r.0, i))\n                    } else {\n                        Some((i, i))\n                    };\n                    found = true;\n                }\n                item.selected = true;\n            }\n        }\n        found\n    }\n\n    pub fn select_rect(&mut self, rect: Rectangle, mod_ctrl: bool, mod_shift: bool) {\n        if let Some(ref mut items) = self.items_opt {\n            for item in items.iter_mut() {\n                let was_overlapped = item.overlaps_drag_rect;\n                item.overlaps_drag_rect = item.rect_opt.get().is_some_and(|r| r.intersects(&rect));\n\n                item.selected = if mod_ctrl || mod_shift {\n                    if was_overlapped == item.overlaps_drag_rect {\n                        item.selected\n                    } else {\n                        !item.selected\n                    }\n                } else {\n                    item.overlaps_drag_rect\n                };\n            }\n        }\n    }\n\n    pub fn select_focus_id(&self) -> Option<widget::Id> {\n        let items = self.items_opt.as_ref()?;\n        let item = items.get(self.select_focus?)?;\n        Some(item.button_id.clone())\n    }\n\n    fn select_focus_pos_opt(&self) -> Option<(usize, usize)> {\n        let items = self.items_opt.as_ref()?;\n        let item = items.get(self.select_focus?)?;\n        item.pos_opt.get()\n    }\n\n    pub(crate) fn select_focus_scroll(&mut self) -> Option<AbsoluteOffset> {\n        let items = self.items_opt.as_ref()?;\n        let item = items.get(self.select_focus?)?;\n        let rect = item.rect_opt.get()?;\n\n        //TODO: move to function\n        let visible_rect = {\n            let point = match self.scroll_opt {\n                Some(offset) => Point::new(0.0, offset.y),\n                None => Point::new(0.0, 0.0),\n            };\n            let size = self\n                .item_view_size_opt\n                .get()\n                .unwrap_or_else(|| Size::new(0.0, 0.0));\n            Rectangle::new(point, size)\n        };\n\n        if rect.y < visible_rect.y {\n            // Scroll up to rect\n            self.scroll_opt = Some(AbsoluteOffset { x: 0.0, y: rect.y });\n            self.scroll_opt\n        } else if (rect.y + rect.height) > (visible_rect.y + visible_rect.height) {\n            // Scroll down to rect\n            self.scroll_opt = Some(AbsoluteOffset {\n                x: 0.0,\n                y: rect.y + rect.height - visible_rect.height,\n            });\n            self.scroll_opt\n        } else {\n            // Do not scroll\n            None\n        }\n    }\n\n    fn select_range_start_pos_opt(&self) -> Option<(usize, usize)> {\n        let items = self.items_opt.as_ref()?;\n        let item = items.get(self.select_range.map(|r| r.0)?)?;\n        item.pos_opt.get()\n    }\n\n    fn select_first_pos_opt(&self) -> Option<(usize, usize)> {\n        let items = self.items_opt.as_ref()?;\n        let mut first = None;\n        for item in items {\n            if !item.selected {\n                continue;\n            }\n\n            let (row, col) = match item.pos_opt.get() {\n                Some(some) => some,\n                None => continue,\n            };\n\n            first = Some(match first {\n                Some((first_row, first_col)) => match row.cmp(&first_row) {\n                    Ordering::Less => (row, col),\n                    Ordering::Equal => (row, col.min(first_row)),\n                    Ordering::Greater => (first_row, first_col),\n                },\n                None => (row, col),\n            });\n        }\n        first\n    }\n\n    fn select_last_pos_opt(&self) -> Option<(usize, usize)> {\n        let items = self.items_opt.as_ref()?;\n        let mut last = None;\n        for item in items {\n            if !item.selected {\n                continue;\n            }\n\n            let (row, col) = match item.pos_opt.get() {\n                Some(some) => some,\n                None => continue,\n            };\n\n            last = Some(match last {\n                Some((last_row, last_col)) => match row.cmp(&last_row) {\n                    Ordering::Greater => (row, col),\n                    Ordering::Equal => (row, col.max(last_row)),\n                    Ordering::Less => (last_row, last_col),\n                },\n                None => (row, col),\n            });\n        }\n        last\n    }\n\n    fn trigger_async_decode(&mut self) -> Vec<Command> {\n        // Only trigger decode in gallery mode for the currently selected image\n        if !self.gallery {\n            return Vec::new();\n        }\n\n        let Some(index) = self.select_focus else {\n            return Vec::new();\n        };\n\n        let Some(items) = &self.items_opt else {\n            return Vec::new();\n        };\n\n        let Some(item) = items.get(index) else {\n            return Vec::new();\n        };\n\n        let Some(ItemThumbnail::Image(_, original_dims)) = &item.thumbnail_opt else {\n            return Vec::new();\n        };\n\n        if let Some((w, h)) = original_dims\n            && !should_use_tiling(*w, *h)\n        {\n            return Vec::new();\n        }\n\n        let Some(path) = item.path_opt() else {\n            return Vec::new();\n        };\n\n        // Clone path to avoid borrow checker issues\n        let path = path.to_path_buf();\n\n        // Get display size for adaptive resolution\n        let display_dimensions = self\n            .size_opt\n            .get()\n            .map(|size| (size.width as u32, size.height as u32));\n\n        // Try to decode the image using LargeImageManager with adaptive resolution\n        let (should_decode, target_dimensions, generation) = self\n            .large_image_manager\n            .try_decode(&path, display_dimensions);\n        if should_decode {\n            vec![Command::Iced(\n                cosmic::iced::Task::perform(\n                    decode_large_image(path, target_dimensions),\n                    move |result| {\n                        result\n                            .map(|(path, width, height, pixels)| {\n                                Message::ImageDecoded(\n                                    path,\n                                    width,\n                                    height,\n                                    pixels,\n                                    display_dimensions,\n                                    generation,\n                                )\n                            })\n                            .unwrap_or_else(|| Message::AutoScroll(None))\n                    },\n                )\n                .into(),\n            )]\n        } else {\n            Vec::new()\n        }\n    }\n\n    pub fn change_location(&mut self, location: &Location, history_i_opt: Option<usize>) {\n        self.location = location.normalize();\n        self.location_ancestors = self.location.ancestors();\n        self.location_title = self.location.title();\n        self.context_menu = None;\n        self.edit_location = None;\n        self.items_opt = None;\n        //TODO: remember scroll by location?\n        self.scroll_opt = None;\n        self.select_focus = None;\n        self.search_context = None;\n        if let Some(history_i) = history_i_opt {\n            // Navigating in history\n            self.history_i = history_i;\n        } else {\n            // Truncate history to remove next entries\n            self.history.truncate(self.history_i + 1);\n\n            // Compact consecutive matching paths\n            {\n                let mut remove = false;\n                if let Some(last_location) = self.history.last() {\n                    if let Location::Network(last_uri, ..) = last_location\n                        && let Location::Network(uri, ..) = location\n                    {\n                        remove = last_uri == uri;\n                    } else if let Some(last_path) = last_location.path_opt()\n                        && let Some(path) = location.path_opt()\n                    {\n                        remove = last_path == path;\n                    }\n                }\n                if remove {\n                    self.history.pop();\n                }\n            }\n\n            // Push to the front of history\n            self.history_i = self.history.len();\n            self.history.push(location.clone());\n        }\n    }\n\n    pub fn update(&mut self, message: Message, modifiers: Modifiers) -> Vec<Command> {\n        let mut commands = Vec::new();\n        let mut cd = None;\n        let mut history_i_opt = None;\n        let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();\n        let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();\n        let last_context_menu = self.context_menu;\n        match message {\n            Message::AddNetworkDrive => {\n                commands.push(Command::AddNetworkDrive);\n            }\n            Message::AutoScroll(auto_scroll) => {\n                commands.push(Command::AutoScroll(auto_scroll));\n            }\n            Message::ClickRelease(click_i_opt) => {\n                // Single click to open.\n                if !mod_ctrl && self.config.single_click {\n                    let mut paths_to_open = Vec::new();\n                    if let Some(ref mut items) = self.items_opt {\n                        for (i, item) in items.iter_mut().enumerate() {\n                            if Some(i) == click_i_opt {\n                                if let Some(location) = &item.location_opt {\n                                    if item.metadata.is_dir() {\n                                        cd = Some(location.clone());\n                                    } else if let Some(path) = location.path_opt() {\n                                        paths_to_open.push(path.clone());\n                                    } else {\n                                        log::warn!(\"no path for item {item:?}\");\n                                    }\n                                } else {\n                                    log::warn!(\"no location for item {item:?}\");\n                                }\n                            }\n                        }\n                    }\n                    if !paths_to_open.is_empty() {\n                        commands.push(Command::OpenFile(paths_to_open));\n                    }\n                }\n\n                if click_i_opt != self.clicked.take() {\n                    self.context_menu = None;\n                    self.location_context_menu_index = None;\n                    if let Some(ref mut items) = self.items_opt {\n                        for (i, item) in items.iter_mut().enumerate() {\n                            if mod_ctrl {\n                                if Some(i) == click_i_opt && item.selected {\n                                    item.selected = false;\n                                    self.select_range = None;\n                                }\n                            } else if Some(i) != click_i_opt {\n                                item.selected = false;\n                            }\n                        }\n                    }\n                }\n            }\n            Message::DragEnd => {\n                self.clicked = None;\n                self.watch_drag = true;\n            }\n            Message::DoubleClick(click_i_opt) => {\n                if let Some(clicked_item) = self\n                    .items_opt\n                    .as_ref()\n                    .and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i)))\n                {\n                    if let Some(location) = &clicked_item.location_opt {\n                        if clicked_item.metadata.is_dir() {\n                            cd = Some(location.clone());\n                        } else if let Some(path) = location.path_opt() {\n                            commands.push(Command::OpenFile(vec![path.clone()]));\n                        } else {\n                            log::warn!(\"no path for item {clicked_item:?}\");\n                        }\n                    } else {\n                        log::warn!(\"no location for item {clicked_item:?}\");\n                    }\n                } else {\n                    log::warn!(\"no item for click index {click_i_opt:?}\");\n                }\n            }\n            Message::Click(click_i_opt) => {\n                self.selected_clicked = false;\n                self.context_menu = None;\n                self.edit_location = None;\n                self.location_context_menu_index = None;\n                if click_i_opt.is_none() {\n                    self.clicked = click_i_opt;\n                }\n\n                if mod_shift {\n                    if let Some(click_i) = click_i_opt {\n                        self.select_range = self\n                            .select_range\n                            .map_or(Some((click_i, click_i)), |r| Some((r.0, click_i)));\n                        if let Some(range) = self.select_range {\n                            let range_min = range.0.min(range.1);\n                            let range_max = range.0.max(range.1);\n                            // A sorted tab's items can't be linearly selected\n                            // Let's say we have:\n                            // index | file\n                            // 0     | file0\n                            // 1     | file1\n                            // 2     | file2\n                            // This is both the default sort and internal ordering\n                            // When sorted it may be displayed as:\n                            // 1     | file1\n                            // 0     | file0\n                            // 2     | file2\n                            // However, the internal ordering is still the same thus\n                            // linearly selecting items doesn't work. Shift selecting\n                            // file0 and file2 would select indices 0 to 2 when it should\n                            // select indices 0 AND 2 from items_opt\n                            let indices: Vec<_> = self\n                                .column_sort()\n                                .map(|sorted| sorted.into_iter().map(|(i, _)| i).collect())\n                                .unwrap_or_else(|| {\n                                    let len = self\n                                        .items_opt\n                                        .as_deref()\n                                        .map(<[Item]>::len)\n                                        .unwrap_or_default();\n                                    (0..len).collect()\n                                });\n\n                            // Find the true indices for the min and max element w.r.t.\n                            // a sorted tab.\n                            let min = indices\n                                .iter()\n                                .copied()\n                                .position(|offset| offset == range_min)\n                                .unwrap_or_default();\n                            // We can't skip `min_real` elements here because the index of\n                            // `max` may actually be before `min` in a sorted tab\n                            let max = indices\n                                .iter()\n                                .copied()\n                                .position(|offset| offset == range_max)\n                                .unwrap_or(indices.len());\n                            let min_real = min.min(max);\n                            let max_real = max.max(min);\n\n                            if let Some(ref mut items) = self.items_opt {\n                                for index in indices\n                                    .into_iter()\n                                    .skip(min_real)\n                                    .take(max_real - min_real + 1)\n                                {\n                                    if let Some(item) = items.get_mut(index) {\n                                        if item.hidden {\n                                            if self.config.show_hidden {\n                                                item.selected = true;\n                                            }\n                                        } else {\n                                            item.selected = true;\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        self.clicked = click_i_opt;\n                        self.select_focus = click_i_opt;\n                        self.selected_clicked = true;\n                    }\n                } else {\n                    let dont_unset = mod_ctrl\n                        || self.column_sort().is_some_and(|l| {\n                            l.iter()\n                                .any(|&(e_i, e)| Some(e_i) == click_i_opt && e.selected)\n                        });\n                    if let Some(ref mut items) = self.items_opt {\n                        for (i, item) in items.iter_mut().enumerate() {\n                            if Some(i) == click_i_opt {\n                                // Filter out selection if it does not match dialog kind\n                                if let Mode::Dialog(dialog) = &self.mode {\n                                    let item_is_dir = item.metadata.is_dir();\n                                    if item_is_dir != dialog.is_dir() {\n                                        // Allow selecting folder if dialog is for files to make it\n                                        // possible to double click\n                                        //TODO: clear any other selection when selecting a folder\n                                        if !item_is_dir {\n                                            continue;\n                                        }\n                                    }\n                                }\n                                if !item.selected {\n                                    self.clicked = click_i_opt;\n                                    item.selected = true;\n                                }\n                                self.select_range = Some((i, i));\n                                self.select_focus = click_i_opt;\n                                self.selected_clicked = true;\n                            } else if !dont_unset && item.selected {\n                                self.clicked = click_i_opt;\n                                item.selected = false;\n                            }\n                        }\n                    }\n                }\n            }\n            Message::Config(config) => {\n                // View is preserved for existing tabs\n                let view = self.config.view;\n                let military_time_changed = self.config.military_time != config.military_time;\n                let show_hidden_changed = self.config.show_hidden != config.show_hidden;\n                self.config = config;\n                self.config.view = view;\n                if military_time_changed {\n                    self.date_time_formatter = date_time_formatter(self.config.military_time);\n                    self.time_formatter = time_formatter(self.config.military_time);\n                }\n                if show_hidden_changed && let Location::Search(path, term, ..) = &self.location {\n                    cd = Some(Location::Search(\n                        path.clone(),\n                        term.clone(),\n                        self.config.show_hidden,\n                        Instant::now(),\n                    ));\n                }\n                // Unhighlight all items when config changes\n                if let Some(ref mut items) = self.items_opt {\n                    for item in items.iter_mut() {\n                        item.highlighted = false;\n                    }\n                }\n            }\n            Message::ContextAction(action) => {\n                // Close context menu\n                self.context_menu = None;\n\n                commands.push(Command::Action(action));\n            }\n            Message::RunContextAction(action) => {\n                self.context_menu = None;\n\n                commands.push(Command::RunContextAction(action));\n            }\n            Message::ContextMenu(point_opt, _) => {\n                self.edit_location = None;\n                self.context_menu = point_opt;\n                self.location_context_menu_index = None;\n\n                //TODO: hack for clearing selecting when right clicking empty space\n                if self.context_menu.is_some()\n                    && self.last_right_click.take().is_none()\n                    && let Some(ref mut items) = self.items_opt\n                {\n                    for item in items.iter_mut() {\n                        item.selected = false;\n                    }\n                }\n            }\n            Message::LocationContextMenuPoint(point_opt) => {\n                self.context_menu = None;\n                self.location_context_menu_point = point_opt;\n            }\n            Message::LocationContextMenuIndex(p, index_opt) => {\n                self.context_menu = None;\n                self.location_context_menu_point = p;\n                self.location_context_menu_index = index_opt;\n            }\n            Message::LocationMenuAction(action) => {\n                self.location_context_menu_index = None;\n                let path_for_index = |ancestor_index| {\n                    self.location\n                        .path_opt()\n                        .and_then(|path| path.ancestors().nth(ancestor_index))\n                        .map(Path::to_path_buf)\n                };\n                match action {\n                    LocationMenuAction::OpenInNewTab(ancestor_index) => {\n                        if let Some(path) = path_for_index(ancestor_index) {\n                            commands.push(Command::OpenInNewTab(path));\n                        }\n                    }\n                    LocationMenuAction::OpenInNewWindow(ancestor_index) => {\n                        if let Some(path) = path_for_index(ancestor_index) {\n                            commands.push(Command::OpenInNewWindow(path));\n                        }\n                    }\n                    LocationMenuAction::Preview(ancestor_index) => {\n                        if let Some(path) = path_for_index(ancestor_index) {\n                            //TODO: blocking code, run in command\n                            match item_from_path(&path, IconSizes::default()) {\n                                Ok(item) => {\n                                    commands.push(Command::Preview(PreviewKind::Custom(\n                                        PreviewItem(Box::new(item)),\n                                    )));\n                                }\n                                Err(err) => {\n                                    log::warn!(\n                                        \"failed to get item from path {}: {}\",\n                                        path.display(),\n                                        err\n                                    );\n                                }\n                            }\n                        }\n                    }\n                    LocationMenuAction::AddToSidebar(ancestor_index) => {\n                        if let Some(path) = path_for_index(ancestor_index) {\n                            commands.push(Command::AddToSidebar(path));\n                        } else {\n                            log::warn!(\n                                \"no ancestor {ancestor_index} for location {:?}\",\n                                self.location\n                            );\n                        }\n                    }\n                }\n            }\n            Message::Drag(rect_opt) => {\n                self.watch_drag = false;\n                if let Some(rect) = rect_opt {\n                    self.context_menu = None;\n                    self.location_context_menu_index = None;\n                    if self.mode.multiple() {\n                        self.select_rect(rect, mod_ctrl, mod_shift);\n                    }\n                    if self.select_focus.take().is_some() {\n                        // Unfocus currently focused button\n                        commands.push(Command::Iced(\n                            widget::button::focus(widget::Id::unique()).into(),\n                        ));\n                    }\n                }\n            }\n            Message::EditLocation(edit_location) => {\n                self.edit_location = edit_location;\n                if self.edit_location.is_some() {\n                    commands.push(Command::Iced(\n                        widget::text_input::focus(self.edit_location_id.clone()).into(),\n                    ));\n                }\n            }\n            Message::EditLocationComplete(selected) => {\n                if let Some(mut edit_location) = self.edit_location.take()\n                    && !matches!(edit_location.location, Location::Network(..))\n                {\n                    edit_location.selected = Some(selected);\n                    cd = edit_location.resolve();\n                }\n            }\n            Message::EditLocationEnable => {\n                commands.push(Command::Iced(\n                    widget::text_input::focus(self.edit_location_id.clone()).into(),\n                ));\n                self.edit_location = Some(self.location.clone().into());\n            }\n            Message::EditLocationSubmit => {\n                if let Some(mut edit_location) = self.edit_location.take() {\n                    // Select first completion if current location does not exist\n                    if edit_location.selected.is_none()\n                        && edit_location\n                            .completions\n                            .as_ref()\n                            .is_some_and(|completions| !completions.is_empty())\n                        && edit_location\n                            .location\n                            .path_opt()\n                            .is_some_and(|path| !path.exists())\n                    {\n                        edit_location.selected = Some(0);\n                    }\n\n                    cd = edit_location.resolve();\n                }\n            }\n            Message::EditLocationTab => {\n                if let Some(edit_location) = &mut self.edit_location {\n                    edit_location.select(!modifiers.contains(Modifiers::SHIFT));\n                }\n            }\n            Message::OpenInNewTab(path) => {\n                commands.push(Command::OpenInNewTab(path));\n            }\n            Message::EmptyTrash => {\n                commands.push(Command::EmptyTrash);\n            }\n            #[cfg(feature = \"desktop\")]\n            Message::ExecEntryAction(path, action) => {\n                let lang_id = crate::localize::LANGUAGE_LOADER.current_language();\n                let language = lang_id.language.as_str();\n                match path.map_or_else(\n                    || {\n                        let items = self.items_opt.as_deref()?;\n                        items.iter().find(|&item| item.selected).and_then(|item| {\n                            let location = item.location_opt.as_ref()?;\n                            let path = location.path_opt()?;\n                            cosmic::desktop::load_desktop_file(&[language.into()], path.into())\n                        })\n                    },\n                    |path| cosmic::desktop::load_desktop_file(&[language.into()], path),\n                ) {\n                    Some(entry) => commands.push(Command::ExecEntryAction(entry, action)),\n                    None => log::warn!(\"Invalid desktop entry path passed to ExecEntryAction\"),\n                }\n            }\n            Message::Gallery(gallery) => {\n                self.gallery = gallery;\n\n                if gallery {\n                    commands.extend(self.trigger_async_decode());\n                }\n            }\n            Message::GalleryPrevious | Message::GalleryNext => {\n                let mut pos_opt = None;\n                if let Some(mut indices) = self.column_sort() {\n                    if matches!(message, Message::GalleryPrevious) {\n                        indices.reverse();\n                    }\n                    let mut found = false;\n                    for (index, item) in indices {\n                        if self.select_focus.is_none() {\n                            found = true;\n                        }\n                        if self.select_focus == Some(index) {\n                            found = true;\n                            continue;\n                        }\n                        if found && item.can_gallery() {\n                            pos_opt = item.pos_opt.get();\n                            if pos_opt.is_some() {\n                                break;\n                            }\n                        }\n                    }\n                }\n                if let Some((row, col)) = pos_opt {\n                    // Should mod_shift be available?\n                    self.select_position(row, col, mod_shift);\n\n                    commands.extend(self.trigger_async_decode());\n                }\n                if let Some(offset) = self.select_focus_scroll() {\n                    commands.push(Command::Iced(\n                        scrollable::scroll_to(\n                            self.scrollable_id.clone(),\n                            AbsoluteOffset {\n                                x: Some(offset.x),\n                                y: Some(offset.y),\n                            },\n                        )\n                        .into(),\n                    ));\n                }\n                if let Some(id) = self.select_focus_id() {\n                    commands.push(Command::Iced(widget::button::focus(id).into()));\n                }\n            }\n            Message::GalleryToggle => {\n                if let Some(indices) = self.column_sort() {\n                    for (_, item) in &indices {\n                        if item.selected && item.can_gallery() {\n                            self.gallery = !self.gallery;\n\n                            if self.gallery {\n                                commands.extend(self.trigger_async_decode());\n                            }\n                            break;\n                        }\n                    }\n                }\n            }\n            Message::GoNext => {\n                if let Some(history_i) = self.history_i.checked_add(1)\n                    && let Some(location) = self.history.get(history_i)\n                {\n                    cd = Some(location.clone());\n                    history_i_opt = Some(history_i);\n                }\n            }\n            Message::GoPrevious => {\n                if let Some(history_i) = self.history_i.checked_sub(1)\n                    && let Some(location) = self.history.get(history_i)\n                {\n                    cd = Some(location.clone());\n                    history_i_opt = Some(history_i);\n                }\n            }\n            Message::ItemDown => {\n                if let Some(edit_location) = &mut self.edit_location {\n                    edit_location.select(true);\n                } else if self.gallery {\n                    commands.append(&mut self.update(Message::GalleryNext, modifiers));\n                } else {\n                    if let Some((row, col)) =\n                        self.select_focus_pos_opt().or(self.select_last_pos_opt())\n                    {\n                        if self.select_focus.is_none() {\n                            // Select last item in current selection to focus it.\n                            self.select_position(row, col, mod_shift);\n                        }\n\n                        //TODO: Shift modifier should select items in between\n                        // Try to select item in next row\n                        if !self.select_position(row + 1, col, mod_shift) {\n                            // Ensure current item is still selected if there are no other items\n                            self.select_position(row, col, mod_shift);\n                        }\n                    } else {\n                        // Select first item\n                        //TODO: select first in scroll\n                        self.select_position(0, 0, mod_shift);\n                    }\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::ItemLeft => {\n                if self.gallery {\n                    commands.append(&mut self.update(Message::GalleryPrevious, modifiers));\n                } else {\n                    if let Some((row, col)) =\n                        self.select_focus_pos_opt().or(self.select_first_pos_opt())\n                    {\n                        if self.select_focus.is_none() {\n                            // Select first item in current selection to focus it.\n                            self.select_position(row, col, mod_shift);\n                        }\n\n                        // Try to select previous item in current row\n                        if !col\n                            .checked_sub(1)\n                            .is_some_and(|col| self.select_position(row, col, mod_shift))\n                        {\n                            // Try to select last item in previous row\n                            if !row.checked_sub(1).is_some_and(|row| {\n                                let mut col = 0;\n                                if let Some(ref items) = self.items_opt {\n                                    for item in items {\n                                        match item.pos_opt.get() {\n                                            Some((item_row, item_col)) if item_row == row => {\n                                                col = col.max(item_col);\n                                            }\n                                            _ => continue,\n                                        }\n                                    }\n                                }\n                                self.select_position(row, col, mod_shift)\n                            }) {\n                                // Ensure current item is still selected if there are no other items\n                                self.select_position(row, col, mod_shift);\n                            }\n                        }\n                    } else {\n                        // Select first item\n                        //TODO: select first in scroll\n                        self.select_position(0, 0, mod_shift);\n                    }\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::ItemRight => {\n                if self.gallery {\n                    commands.append(&mut self.update(Message::GalleryNext, modifiers));\n                } else {\n                    if let Some((row, col)) =\n                        self.select_focus_pos_opt().or(self.select_last_pos_opt())\n                    {\n                        if self.select_focus.is_none() {\n                            // Select last item in current selection to focus it.\n                            self.select_position(row, col, mod_shift);\n                        }\n                        // Try to select next item in current row\n                        if !self.select_position(row, col + 1, mod_shift) {\n                            // Try to select first item in next row\n                            if !self.select_position(row + 1, 0, mod_shift) {\n                                // Ensure current item is still selected if there are no other items\n                                self.select_position(row, col, mod_shift);\n                            }\n                        }\n                    } else {\n                        // Select first item\n                        //TODO: select first in scroll\n                        self.select_position(0, 0, mod_shift);\n                    }\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::ItemUp => {\n                if let Some(edit_location) = &mut self.edit_location {\n                    edit_location.select(false);\n                } else if self.gallery {\n                    commands.append(&mut self.update(Message::GalleryPrevious, modifiers));\n                } else {\n                    if let Some((row, col)) =\n                        self.select_focus_pos_opt().or(self.select_first_pos_opt())\n                    {\n                        if self.select_focus.is_none() {\n                            // Select first item in current selection to focus it.\n                            self.select_position(row, col, mod_shift);\n                        }\n\n                        //TODO: Shift modifier should select items in between\n                        // Try to select item in last row\n                        if !row\n                            .checked_sub(1)\n                            .is_some_and(|row| self.select_position(row, col, mod_shift))\n                        {\n                            // Ensure current item is still selected if there are no other items\n                            self.select_position(row, col, mod_shift);\n                        }\n                    } else {\n                        // Select first item\n                        //TODO: select first in scroll\n                        self.select_position(0, 0, mod_shift);\n                    }\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::Location(location) => {\n                // Workaround to support favorited files\n                match &location {\n                    Location::Path(path) => {\n                        if path.is_dir() {\n                            cd = Some(location);\n                        } else {\n                            commands.push(Command::OpenFile(vec![path.clone()]));\n                        }\n                    }\n                    _ => {\n                        cd = Some(location);\n                    }\n                }\n            }\n            Message::LocationUp => {\n                // Sets location to the path's parent\n                // Does nothing if path is root or location is Trash\n                if let Location::Path(ref path) = self.location\n                    && let Some(parent) = path.parent()\n                {\n                    cd = Some(Location::Path(parent.to_owned()));\n                }\n            }\n            Message::Open(path_opt) => {\n                match path_opt {\n                    Some(path) => {\n                        if path.is_dir() {\n                            cd = Some(Location::Path(path));\n                        } else {\n                            commands.push(Command::OpenFile(vec![path]));\n                        }\n                    }\n                    // Open selected items\n                    None => {\n                        enum ResolveResult {\n                            Open(Option<PathBuf>),\n                            OpenInTab(Option<PathBuf>),\n                            OpenTrash,\n                            OpenProperties,\n                            Cd(Location),\n                            Skip,\n                        }\n                        fn resolve_item(\n                            item: &Item,\n                            mode: &Mode,\n                            is_only_one_selected: bool,\n                        ) -> ResolveResult {\n                            if !item.selected {\n                                return ResolveResult::Skip;\n                            }\n\n                            let location = match &item.location_opt {\n                                Some(l) => l,\n                                None => return ResolveResult::OpenProperties,\n                            };\n\n                            let path_opt = location.path_opt();\n\n                            if item.metadata.is_dir() {\n                                match mode {\n                                    Mode::App => {\n                                        if is_only_one_selected {\n                                            ResolveResult::Cd(location.clone())\n                                        } else {\n                                            ResolveResult::OpenInTab(path_opt.cloned())\n                                        }\n                                    }\n                                    Mode::Desktop => match location {\n                                        Location::Trash => ResolveResult::OpenTrash,\n                                        _ => ResolveResult::Open(path_opt.cloned()),\n                                    },\n                                    Mode::Dialog(_) => {\n                                        if is_only_one_selected {\n                                            ResolveResult::Cd(location.clone())\n                                        } else {\n                                            ResolveResult::Skip\n                                        }\n                                    }\n                                }\n                            } else {\n                                ResolveResult::Open(path_opt.cloned())\n                            }\n                        }\n                        let mut open_files = Vec::new();\n                        if let Some(items) = self.items_opt.as_ref() {\n                            let selected_count = items.iter().filter(|i| i.selected).count();\n\n                            for item in items.iter() {\n                                match resolve_item(item, &self.mode, selected_count == 1) {\n                                    ResolveResult::Open(Some(p)) => open_files.push(p),\n                                    ResolveResult::OpenInTab(Some(p)) => {\n                                        commands.push(Command::OpenInNewTab(p))\n                                    }\n                                    ResolveResult::Cd(loc) => cd = Some(loc),\n                                    ResolveResult::OpenTrash => commands.push(Command::OpenTrash),\n                                    ResolveResult::OpenProperties => {} //TODO: open properties?\n                                    _ => {}\n                                }\n                            }\n                        }\n                        if !open_files.is_empty() {\n                            commands.push(Command::OpenFile(open_files));\n                        }\n                    }\n                }\n            }\n            Message::Reload => {\n                //TODO: support keeping selected locations without paths\n                let selected_paths = self\n                    .selected_locations()\n                    .into_iter()\n                    .filter_map(Location::into_path_opt)\n                    .collect();\n                let location = self.location.clone();\n                self.change_location(&location, None);\n                commands.push(Command::ChangeLocation(\n                    self.title(),\n                    location,\n                    Some(selected_paths),\n                ));\n            }\n            Message::RightClick(_point_opt, click_i_opt) => {\n                if mod_ctrl || mod_shift {\n                    self.update(Message::Click(click_i_opt), modifiers);\n                }\n                if let Some(ref mut items) = self.items_opt\n                    && !click_i_opt\n                        .is_some_and(|click_i| items.get(click_i).is_some_and(|x| x.selected))\n                {\n                    // If item not selected, clear selection on other items\n                    for (i, item) in items.iter_mut().enumerate() {\n                        item.selected = Some(i) == click_i_opt;\n                    }\n                }\n                //TODO: hack for clearing selecting when right clicking empty space\n                self.last_right_click = click_i_opt;\n            }\n            Message::MiddleClick(click_i) => {\n                if mod_ctrl || mod_shift {\n                    self.update(Message::Click(Some(click_i)), modifiers);\n                } else {\n                    if let Some(ref mut items) = self.items_opt {\n                        for (i, item) in items.iter_mut().enumerate() {\n                            item.selected = i == click_i;\n                        }\n                        self.select_range = Some((click_i, click_i));\n                    }\n                    if let Some(clicked_item) =\n                        self.items_opt.as_ref().and_then(|items| items.get(click_i))\n                    {\n                        if let Some(path) = clicked_item.path_opt() {\n                            if clicked_item.metadata.is_dir() {\n                                //cd = Some(Location::Path(path.clone()));\n                                commands.push(Command::OpenInNewTab(path.clone()));\n                            } else {\n                                commands.push(Command::OpenFile(vec![path.clone()]));\n                            }\n                        } else {\n                            log::warn!(\"no path for item {clicked_item:?}\");\n                        }\n                    } else {\n                        log::warn!(\"no item for click index {click_i:?}\");\n                    }\n                }\n            }\n            Message::HighlightDeactivate(i) => {\n                self.watch_drag = true;\n                if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {\n                    item.highlighted = false;\n                }\n            }\n            Message::HighlightActivate(i) => {\n                self.watch_drag = true;\n                if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {\n                    item.highlighted = true;\n                }\n            }\n            Message::Resize(viewport) => {\n                // Scroll to ensure focused item still in view\n                if self.viewport_opt.map(|v| v.size()) != Some(viewport.size())\n                    && let Some(offset) = self.select_focus_scroll()\n                {\n                    commands.push(Command::Iced(\n                        scrollable::scroll_to(\n                            self.scrollable_id.clone(),\n                            AbsoluteOffset {\n                                x: Some(offset.x),\n                                y: Some(offset.y),\n                            },\n                        )\n                        .into(),\n                    ));\n                }\n\n                self.viewport_opt = Some(viewport);\n            }\n            Message::Scroll(viewport) => {\n                self.scroll_opt = Some(viewport.absolute_offset());\n                self.watch_drag = true;\n            }\n            Message::ScrollTab(scroll_speed) => {\n                commands.push(Command::Iced(\n                    scrollable::scroll_by(\n                        self.scrollable_id.clone(),\n                        AbsoluteOffset {\n                            x: 0.0,\n                            y: scroll_speed,\n                        },\n                    )\n                    .into(),\n                ));\n            }\n            Message::ScrollToFocused => {\n                if let Some(offset) = self.select_focus_scroll() {\n                    commands.push(Command::Iced(\n                        scrollable::scroll_to(\n                            self.scrollable_id.clone(),\n                            AbsoluteOffset {\n                                x: Some(offset.x),\n                                y: Some(offset.y),\n                            },\n                        )\n                        .into(),\n                    ));\n                }\n            }\n            Message::SearchContext(location, context) => {\n                if location == self.location {\n                    self.search_context = context.0;\n                } else {\n                    log::warn!(\n                        \"search context provided for {:?} instead of {:?}\",\n                        location,\n                        self.location\n                    );\n                }\n            }\n            Message::SearchReady(finished) => {\n                if let Some(context) = &mut self.search_context {\n                    if let Some(items) = &mut self.items_opt {\n                        if finished || context.ready.swap(false, atomic::Ordering::SeqCst) {\n                            let duration = Instant::now();\n                            while let Ok(search_item) = context.results_rx.try_recv() {\n                                //TODO: combine this with column_sort logic, they must match!\n                                let index =\n                                    if let SearchItem::Path(_, _, ref metadata) = search_item {\n                                        let item_modified = metadata.modified().ok();\n                                        match items.binary_search_by(|other| {\n                                            item_modified.cmp(&other.metadata.modified())\n                                        }) {\n                                            Ok(index) => index,\n                                            Err(index) => index,\n                                        }\n                                    } else {\n                                        items.len()\n                                    };\n\n                                if index < MAX_SEARCH_RESULTS {\n                                    //TODO: use correct IconSizes\n                                    let item =\n                                        item_from_search_item(search_item, IconSizes::default());\n                                    items.insert(index, item);\n                                }\n                                // Ensure that updates make it to the GUI in a timely manner\n                                if !finished && duration.elapsed() >= MAX_SEARCH_LATENCY {\n                                    break;\n                                }\n                            }\n                        }\n                        if items.len() >= MAX_SEARCH_RESULTS {\n                            items.truncate(MAX_SEARCH_RESULTS);\n                            if let Some(last_modified) =\n                                items.last().and_then(|item| item.metadata.modified())\n                            {\n                                *context.last_modified_opt.write().unwrap() = Some(last_modified);\n                            }\n                        }\n                    } else {\n                        log::warn!(\"search ready but items array is empty\");\n                    }\n                }\n                if finished {\n                    self.search_context = None;\n                }\n            }\n            Message::SelectAll => {\n                self.select_all();\n                if self.select_focus.take().is_some() {\n                    // Unfocus currently focused button\n                    commands.push(Command::Iced(\n                        widget::button::focus(widget::Id::unique()).into(),\n                    ));\n                }\n            }\n            Message::SelectFirst => {\n                if self.select_position(0, 0, mod_shift) {\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::SelectLast => {\n                if let Some(ref items) = self.items_opt\n                    && let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max()\n                    && self.select_position(last_pos.0, last_pos.1, mod_shift)\n                {\n                    if let Some(offset) = self.select_focus_scroll() {\n                        commands.push(Command::Iced(\n                            scrollable::scroll_to(\n                                self.scrollable_id.clone(),\n                                AbsoluteOffset {\n                                    x: Some(offset.x),\n                                    y: Some(offset.y),\n                                },\n                            )\n                            .into(),\n                        ));\n                    }\n                    if let Some(id) = self.select_focus_id() {\n                        commands.push(Command::Iced(widget::button::focus(id).into()));\n                    }\n                }\n            }\n            Message::SetOpenWith(mime, id) => {\n                commands.push(Command::SetOpenWith(mime, id));\n            }\n            Message::SetPermissions(path, mode) => {\n                commands.push(Command::SetPermissions(path, mode));\n            }\n            Message::ShiftPermissions(path_mode_opt, shift, bits) => match path_mode_opt {\n                Some((path, mode)) => commands.push(Command::SetPermissions(\n                    path,\n                    set_mode_part(mode, shift, bits),\n                )),\n                // Shift permissions on all selected items\n                None => {\n                    let mut permissions = Vec::new();\n                    for item in self.items_opt().map_or(Vec::new(), |items| {\n                        items.iter().filter(|item| item.selected).collect()\n                    }) {\n                        #[cfg(unix)]\n                        if let (Some(path), Some(mode)) = (\n                            item.path_opt(),\n                            item.file_metadata().map(|metadata| metadata.mode()),\n                        ) {\n                            permissions.push((path.clone(), set_mode_part(mode, shift, bits)));\n                        }\n                    }\n                    commands.push(Command::SetMultiplePermissions(permissions));\n                }\n            },\n            Message::SetSort(heading_option, dir) => {\n                if !matches!(self.location, Location::Search(..)) {\n                    self.sort_name = heading_option;\n                    self.sort_direction = dir;\n                    if !matches!(self.location, Location::Desktop(..)) {\n                        commands.push(Command::SetSort(\n                            self.location.normalize().to_string(),\n                            heading_option,\n                            self.sort_direction,\n                        ));\n                    }\n                }\n            }\n            Message::TabComplete(path, completions) => {\n                if let Some(edit_location) = &mut self.edit_location\n                    && edit_location.location.path_opt() == Some(&path)\n                {\n                    edit_location.completions = Some(completions);\n                    commands.push(Command::Iced(\n                        widget::text_input::focus(self.edit_location_id.clone()).into(),\n                    ));\n                }\n            }\n            Message::Thumbnail(path, thumbnail) => {\n                if let Some(ref mut items) = self.items_opt {\n                    let location = Location::Path(path);\n                    for item in items.iter_mut() {\n                        if item.location_opt.as_ref() == Some(&location) {\n                            let handle_opt = match &thumbnail {\n                                ItemThumbnail::NotImage => None,\n                                ItemThumbnail::Image(handle, _) => Some(widget::icon::Handle {\n                                    symbolic: false,\n                                    data: widget::icon::Data::Image(handle.clone()),\n                                }),\n                                ItemThumbnail::Svg(handle) => Some(widget::icon::Handle {\n                                    symbolic: false,\n                                    data: widget::icon::Data::Svg(handle.clone()),\n                                }),\n                                //TODO: text thumbnails?\n                                ItemThumbnail::Text(_text) => None,\n                            };\n                            if let Some(handle) = handle_opt {\n                                item.icon_handle_grid.clone_from(&handle);\n                                item.icon_handle_list.clone_from(&handle);\n                                item.icon_handle_list_condensed = handle;\n                            }\n                            item.thumbnail_opt = Some(thumbnail);\n                            break;\n                        }\n                    }\n                }\n            }\n            Message::ImageDecoded(path, width, height, pixels, display_size, generation) => {\n                // Create handle from pre-decoded RGBA data (fast!)\n                let handle = widget::image::Handle::from_rgba(width, height, pixels);\n\n                // Store decoded image handle if generation still matches (not superseded)\n                self.large_image_manager.store_decoded_with_generation(\n                    path,\n                    handle,\n                    display_size,\n                    generation,\n                );\n            }\n            Message::ToggleSort(heading_option) => {\n                if !matches!(self.location, Location::Search(..)) {\n                    let heading_sort = if self.sort_name == heading_option {\n                        !self.sort_direction\n                    } else {\n                        // Default modified to descending, and others to ascending.\n                        heading_option != HeadingOptions::Modified\n                    };\n\n                    if !matches!(self.location, Location::Desktop(..)) {\n                        commands.push(Command::SetSort(\n                            self.location.normalize().to_string(),\n                            heading_option,\n                            heading_sort,\n                        ));\n                    }\n\n                    self.sort_direction = heading_sort;\n                    self.sort_name = heading_option;\n                }\n            }\n            Message::Drop(Some((to, mut from))) => {\n                self.dnd_hovered = None;\n                match to {\n                    Location::Desktop(to, ..)\n                    | Location::Path(to)\n                    | Location::Network(_, _, Some(to)) => {\n                        if let Ok(entries) = fs::read_dir(&to) {\n                            for i in entries.into_iter().filter_map(Result::ok) {\n                                let i = i.path();\n                                from.paths.retain(|p| &i != p);\n                                if from.paths.is_empty() {\n                                    log::info!(\"All dropped files already in target directory.\");\n                                    return commands;\n                                }\n                            }\n                        }\n                        commands.push(Command::DropFiles(to, from));\n                    }\n                    Location::Trash if matches!(from.kind, ClipboardKind::Cut { .. }) => {\n                        commands.push(Command::Delete(from.paths));\n                    }\n                    _ => {\n                        log::warn!(\"{:?} to {:?} is not supported.\", from.kind, to);\n                    }\n                }\n            }\n            Message::Drop(None) => {\n                self.dnd_hovered = None;\n            }\n            Message::DndHover(loc) => {\n                if self\n                    .dnd_hovered\n                    .as_ref()\n                    .is_some_and(|(l, i)| *l == loc && i.elapsed() > HOVER_DURATION)\n                {\n                    cd = Some(loc);\n                }\n            }\n            Message::DndEnter(loc) => {\n                self.dnd_hovered = Some((loc.clone(), Instant::now()));\n                if loc != self.location {\n                    commands.push(Command::Iced(\n                        cosmic::Task::future(async move {\n                            tokio::time::sleep(HOVER_DURATION).await;\n                            Message::DndHover(loc)\n                        })\n                        .into(),\n                    ));\n                }\n            }\n            Message::DndLeave(loc) => {\n                if Some(&loc) == self.dnd_hovered.as_ref().map(|(l, _)| l) {\n                    self.dnd_hovered = None;\n                }\n            }\n            Message::WindowDrag => {\n                commands.push(Command::WindowDrag);\n            }\n            Message::WindowToggleMaximize => {\n                commands.push(Command::WindowToggleMaximize);\n            }\n            Message::ZoomIn => {\n                commands.push(Command::Action(Action::ZoomIn));\n            }\n            Message::ZoomOut => {\n                commands.push(Command::Action(Action::ZoomOut));\n            }\n            Message::DirectorySize(path, dir_size) => {\n                let location = Location::Path(path);\n                if let Some(ref mut item) = self.parent_item_opt\n                    && item.location_opt.as_ref() == Some(&location)\n                {\n                    item.dir_size.clone_from(&dir_size);\n                }\n                if let Some(ref mut items) = self.items_opt {\n                    for item in items.iter_mut() {\n                        if item.location_opt.as_ref() == Some(&location) {\n                            item.dir_size = dir_size;\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        // Scroll to top if needed\n        if self.scroll_opt.is_none() {\n            let offset = AbsoluteOffset { x: 0.0, y: 0.0 };\n            self.scroll_opt = Some(offset);\n            commands.push(Command::Iced(\n                scrollable::scroll_to(\n                    self.scrollable_id.clone(),\n                    AbsoluteOffset {\n                        x: Some(0.0),\n                        y: Some(0.0),\n                    },\n                )\n                .into(),\n            ));\n        }\n\n        // Change directory if requested\n        if let Some(mut location) = cd {\n            location = location.normalize();\n            if matches!(self.mode, Mode::Desktop) {\n                match location {\n                    Location::Path(path) => {\n                        commands.push(Command::OpenFile(vec![path]));\n                    }\n                    Location::Trash => {\n                        commands.push(Command::OpenTrash);\n                    }\n                    _ => {}\n                }\n            } else {\n                // Select parent if location is not directory\n                let mut selected_paths = None;\n                if let Some(path) = location.path_opt()\n                    && !path.is_dir()\n                    && let Some(parent) = path.parent()\n                {\n                    selected_paths = Some(vec![path.clone()]);\n                    location = location.with_path(parent.to_path_buf());\n                }\n                if location != self.location || selected_paths.is_some() {\n                    if location.path_opt().is_none_or(|path| path.is_dir()) {\n                        if selected_paths.is_none() {\n                            selected_paths =\n                                self.location.path_opt().map(|path| vec![path.clone()]);\n                        }\n                        self.change_location(&location, history_i_opt);\n                        commands.push(Command::ChangeLocation(\n                            self.title(),\n                            location,\n                            selected_paths,\n                        ));\n                    } else {\n                        log::warn!(\"tried to cd to {location:?} which is not a directory\");\n                    }\n                }\n            }\n        }\n\n        // Update context menu popup\n        if self.context_menu != last_context_menu {\n            if last_context_menu.is_some() {\n                commands.push(Command::ContextMenu(None, self.window_id));\n            }\n            if let Some(point) = self.context_menu {\n                commands.push(Command::ContextMenu(Some(point), self.window_id));\n            }\n        }\n\n        commands\n    }\n\n    pub(crate) const fn sort_options(&self) -> (HeadingOptions, bool, bool) {\n        match self.location {\n            Location::Search(..) => (HeadingOptions::Modified, false, false),\n            _ => (\n                self.sort_name,\n                self.sort_direction,\n                self.config.folders_first,\n            ),\n        }\n    }\n\n    fn column_sort(&self) -> Option<Vec<(usize, &Item)>> {\n        let check_reverse = |ord: Ordering, sort: bool| {\n            if sort { ord } else { ord.reverse() }\n        };\n        let mut items: Vec<_> = self.items_opt.as_ref()?.iter().enumerate().collect();\n        let (sort_name, sort_direction, folders_first) = self.sort_options();\n        match sort_name {\n            HeadingOptions::Size => {\n                items.sort_by(|a, b| {\n                    // entries take precedence over size\n                    let get_size = |x: &Item| match &x.metadata {\n                        ItemMetadata::Path {\n                            metadata,\n                            children_opt,\n                        } => {\n                            if metadata.is_dir() {\n                                (true, children_opt.unwrap_or_default() as u64)\n                            } else {\n                                (false, metadata.len())\n                            }\n                        }\n                        ItemMetadata::Trash { metadata, .. } => match metadata.size {\n                            trash::TrashItemSize::Entries(entries) => (true, entries as u64),\n                            trash::TrashItemSize::Bytes(bytes) => (false, bytes),\n                        },\n                        ItemMetadata::SimpleDir { entries } => (true, *entries),\n                        ItemMetadata::SimpleFile { size } => (false, *size),\n                        #[cfg(feature = \"gvfs\")]\n                        ItemMetadata::GvfsPath {\n                            size_opt,\n                            children_opt,\n                            ..\n                        } => match children_opt {\n                            Some(child_count) => (true, *child_count as u64),\n                            None => (false, size_opt.unwrap_or_default()),\n                        },\n                    };\n                    let (a_is_entry, a_size) = get_size(a.1);\n                    let (b_is_entry, b_size) = get_size(b.1);\n\n                    //TODO: use folders_first?\n                    match (a_is_entry, b_is_entry) {\n                        (true, false) => Ordering::Less,\n                        (false, true) => Ordering::Greater,\n                        _ => check_reverse(a_size.cmp(&b_size), sort_direction),\n                    }\n                });\n            }\n            HeadingOptions::Name => items.sort_by(|a, b| {\n                if folders_first {\n                    match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {\n                        (true, false) => Ordering::Less,\n                        (false, true) => Ordering::Greater,\n                        _ => check_reverse(\n                            LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),\n                            sort_direction,\n                        ),\n                    }\n                } else {\n                    check_reverse(\n                        LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),\n                        sort_direction,\n                    )\n                }\n            }),\n            HeadingOptions::Modified => {\n                items.sort_by(|a, b| {\n                    let a_modified = a.1.metadata.modified();\n                    let b_modified = b.1.metadata.modified();\n                    if folders_first {\n                        match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {\n                            (true, false) => Ordering::Less,\n                            (false, true) => Ordering::Greater,\n                            _ => check_reverse(a_modified.cmp(&b_modified), sort_direction),\n                        }\n                    } else {\n                        check_reverse(a_modified.cmp(&b_modified), sort_direction)\n                    }\n                });\n            }\n            HeadingOptions::TrashedOn => {\n                let time_deleted = |x: &Item| match &x.metadata {\n                    ItemMetadata::Trash { entry, .. } => Some(entry.time_deleted),\n                    _ => None,\n                };\n\n                items.sort_by(|a, b| {\n                    let a_time_deleted = time_deleted(a.1);\n                    let b_time_deleted = time_deleted(b.1);\n                    if folders_first {\n                        match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {\n                            (true, false) => Ordering::Less,\n                            (false, true) => Ordering::Greater,\n                            _ => check_reverse(a_time_deleted.cmp(&b_time_deleted), sort_direction),\n                        }\n                    } else {\n                        check_reverse(b_time_deleted.cmp(&a_time_deleted), sort_direction)\n                    }\n                });\n            }\n        }\n        Some(items)\n    }\n\n    fn dnd_dest<'a>(\n        &self,\n        location: &Location,\n        element: impl Into<Element<'a, Message>>,\n    ) -> Element<'a, Message> {\n        let location1 = location.clone();\n        let location2 = location.clone();\n        let location3 = location.clone();\n        let is_dnd_hovered = self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(location);\n        let mut container = widget::container(\n            DndDestination::for_data::<ClipboardPaste>(element, move |data, action| {\n                if let Some(mut data) = data {\n                    if action == DndAction::Copy {\n                        Message::Drop(Some((location1.clone(), data)))\n                    } else if action == DndAction::Move {\n                        data.kind = ClipboardKind::Cut { is_dnd: true };\n                        Message::Drop(Some((location1.clone(), data)))\n                    } else {\n                        log::warn!(\"unsupported action: {action:?}\");\n                        Message::Drop(None)\n                    }\n                } else {\n                    Message::Drop(None)\n                }\n            })\n            .on_enter(move |_, _, _| Message::DndEnter(location2.clone()))\n            .on_leave(move || Message::DndLeave(location3.clone())),\n        );\n        // Desktop will not show DnD indicator\n        if is_dnd_hovered && !matches!(self.mode, Mode::Desktop) {\n            container = container.style(|t| {\n                let mut a = widget::container::Style::default();\n                let t = t.cosmic();\n                // todo use theme drop target color\n                let mut bg = t.accent_color();\n                bg.alpha = 0.2;\n                a.background = Some(Color::from(bg).into());\n                a.border = Border {\n                    color: t.accent_color().into(),\n                    width: 1.0,\n                    radius: t.radius_s().into(),\n                };\n                a\n            });\n        }\n        container.into()\n    }\n\n    pub fn gallery_view(&self) -> Element<'_, Message> {\n        let cosmic_theme::Spacing {\n            space_xxs,\n            space_xs,\n            space_m,\n            ..\n        } = theme::spacing();\n\n        //TODO: display error messages when image not found?\n        let mut name_opt = None;\n        let mut element_opt: Option<Element<Message>> = None;\n        if let Some(index) = self.select_focus\n            && let Some(items) = &self.items_opt\n            && let Some(item) = items.get(index)\n        {\n            name_opt = Some(widget::text::heading(&item.display_name));\n            match item\n                .thumbnail_opt\n                .as_ref()\n                .unwrap_or(&ItemThumbnail::NotImage)\n            {\n                ItemThumbnail::NotImage => {}\n                ItemThumbnail::Image(handle, original_dims) => {\n                    // Determine which image to show based on async decode state\n                    let mut is_loading = false;\n                    let mut error_msg_opt = None;\n                    let image_handle = if let Some(path) = item.path_opt() {\n                        if let Some(error_msg) = self.large_image_manager.get_error(path) {\n                            error_msg_opt = Some(error_msg.clone());\n                            handle.clone()\n                        } else if self.large_image_manager.is_decoding(path) {\n                            // Currently decoding (initial or re-decode) --> show cached/thumbnail with loading indicator\n                            is_loading = true;\n                            // Use decoded handle if available (re-decode), otherwise thumbnail (initial decode)\n                            self.large_image_manager\n                                .get_decoded(path)\n                                .cloned()\n                                .unwrap_or_else(|| handle.clone())\n                        } else if let Some(decoded_handle) =\n                            self.large_image_manager.get_decoded(path)\n                        {\n                            // Decoded and not currently decoding --> use it\n                            decoded_handle.clone()\n                        } else if let Some((w, h)) = original_dims {\n                            // Check if image needs tiling\n                            if should_use_tiling(*w, *h) {\n                                // Large image --> show thumbnail only\n                                handle.clone()\n                            } else {\n                                // Normal-sized image --> load full resolution directly\n                                widget::image::Handle::from_path(path)\n                            }\n                        } else {\n                            // No dimensions available --> show thumbnail\n                            handle.clone()\n                        }\n                    } else {\n                        handle.clone()\n                    };\n\n                    let content: cosmic::Element<'_, Message> =\n                        if let Some(error_msg) = error_msg_opt {\n                            widget::column::with_capacity(2)\n                                .push(widget::image(image_handle))\n                                .push(widget::text(format!(\"⚠ {}\", error_msg)).size(13))\n                                .padding(space_xs)\n                                .align_x(cosmic::iced::Alignment::Center)\n                                .into()\n                        } else if is_loading {\n                            widget::column::with_capacity(2)\n                                .push(widget::image(image_handle))\n                                .push(widget::text(\"Loading higher resolution...\").size(14))\n                                .padding(space_xs)\n                                .align_x(cosmic::iced::Alignment::Center)\n                                .into()\n                        } else {\n                            //TODO: use widget::image::viewer, when its zoom can be reset\n                            crate::load_image::loaded_image(image_handle).into()\n                        };\n\n                    element_opt = Some(widget::container(content).center(Length::Fill).into());\n                }\n                ItemThumbnail::Svg(handle) => {\n                    element_opt = Some(\n                        widget::svg(handle.clone())\n                            .width(Length::Fill)\n                            .height(Length::Fill)\n                            .into(),\n                    );\n                }\n                ItemThumbnail::Text(text) => {\n                    element_opt = Some(\n                        widget::container(widget::text_editor(text).padding(space_xxs).class(\n                            cosmic::theme::iced::TextEditor::Custom(Box::new(text_editor_class)),\n                        ))\n                        .center(Length::Fill)\n                        .into(),\n                    );\n                }\n            }\n        }\n\n        let mut column = widget::column::with_capacity(2);\n        column = column.push(widget::space::vertical().height(Length::Fixed(space_m.into())));\n        {\n            let mut row = widget::row::with_capacity(5).align_y(Alignment::Center);\n            row = row.push(widget::space::horizontal());\n            if let Some(name) = name_opt {\n                row = row.push(name);\n            }\n            row = row.push(widget::space::horizontal());\n            row = row.push(\n                widget::button::icon(widget::icon::from_name(\"window-close-symbolic\"))\n                    .class(theme::Button::Standard)\n                    .on_press(Message::Gallery(false)),\n            );\n            row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into())));\n            // This mouse area provides window drag while the header bar is hidden\n            let mouse_area = mouse_area::MouseArea::new(row)\n                .on_press(|_| Message::WindowDrag)\n                .on_double_click(|_| Message::WindowToggleMaximize);\n            column = column.push(mouse_area);\n        }\n        {\n            let mut row = widget::row::with_capacity(7).align_y(Alignment::Center);\n            row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into())));\n            row = row.push(\n                widget::button::icon(widget::icon::from_name(\"go-previous-symbolic\"))\n                    .padding(space_xs)\n                    .class(theme::Button::Standard)\n                    .on_press(Message::GalleryPrevious),\n            );\n            row = row.push(widget::space::horizontal().width(Length::Fixed(space_xxs.into())));\n            if let Some(element) = element_opt {\n                row = row.push(element);\n            } else {\n                //TODO: what to do when no image?\n                row = row.push(space::horizontal().width(Length::Fill));\n                row = row.push(space::vertical().height(Length::Fill));\n            }\n            row = row.push(widget::space::horizontal().width(Length::Fixed(space_xxs.into())));\n            row = row.push(\n                widget::button::icon(widget::icon::from_name(\"go-next-symbolic\"))\n                    .padding(space_xs)\n                    .class(theme::Button::Standard)\n                    .on_press(Message::GalleryNext),\n            );\n            row = row.push(widget::space::horizontal().width(Length::Fixed(space_m.into())));\n            column = column.push(row);\n        }\n\n        widget::container(column)\n            .width(Length::Fill)\n            .height(Length::Fill)\n            .style(|theme| {\n                let cosmic = theme.cosmic();\n                let mut bg = cosmic.bg_color();\n                bg.alpha = 0.75;\n                widget::container::Style {\n                    background: Some(Color::from(bg).into()),\n                    ..Default::default()\n                }\n            })\n            .into()\n    }\n\n    pub fn location_view(&self) -> Element<'_, Message> {\n        //TODO: responsiveness is done in a hacky way, potentially move this to a custom widget?\n        fn text_width<'a>(\n            content: &'a str,\n            font: font::Font,\n            font_size: f32,\n            line_height: f32,\n        ) -> f32 {\n            let text: text::Text<&'a str, font::Font> = text::Text {\n                content,\n                bounds: Size::INFINITE,\n                size: font_size.into(),\n                line_height: text::LineHeight::Absolute(line_height.into()),\n                font,\n                align_x: text::Alignment::Left,\n                align_y: Vertical::Top,\n                shaping: text::Shaping::default(),\n                wrapping: text::Wrapping::None,\n                ellipsize: text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1)),\n            };\n            graphics::text::Paragraph::with_text(text)\n                .min_bounds()\n                .width\n        }\n        fn text_width_body(content: &str) -> f32 {\n            //TODO: should libcosmic set the font when using widget::text::body?\n            text_width(content, font::default(), 14.0, 20.0)\n        }\n        fn text_width_heading(content: &str) -> f32 {\n            text_width(content, font::semibold(), 14.0, 20.0)\n        }\n\n        let cosmic_theme::Spacing {\n            space_xxxs,\n            space_xxs,\n            space_s,\n            space_m,\n            ..\n        } = theme::spacing();\n\n        let size = self.size_opt.get().unwrap_or(Size::new(0.0, 0.0));\n\n        let mut row = widget::row::with_capacity(5)\n            .align_y(Alignment::Center)\n            .padding([space_xxxs, 0]);\n        let mut w = 0.0;\n\n        let mut prev_button =\n            widget::button::custom(widget::icon::from_name(\"go-previous-symbolic\").size(16))\n                .padding(space_xxs)\n                .class(theme::Button::Icon);\n        if self.history_i > 0 && !self.history.is_empty() {\n            prev_button = prev_button.on_press(Message::GoPrevious);\n        }\n        row = row.push(prev_button);\n        w += f32::from(space_xxs).mul_add(2.0, 16.0);\n\n        let mut next_button =\n            widget::button::custom(widget::icon::from_name(\"go-next-symbolic\").size(16))\n                .padding(space_xxs)\n                .class(theme::Button::Icon);\n        if self.history_i + 1 < self.history.len() {\n            next_button = next_button.on_press(Message::GoNext);\n        }\n        row = row.push(next_button);\n        w += f32::from(space_xxs).mul_add(2.0, 16.0);\n\n        row = row.push(widget::space::horizontal().width(Length::Fixed(space_s.into())));\n        w += f32::from(space_s);\n\n        //TODO: allow resizing?\n        let name_width = 300.0;\n        let modified_width = 200.0;\n        let size_width = 100.0;\n        let condensed = size.width < (name_width + modified_width + size_width);\n\n        let (sort_name, sort_direction, _) = self.sort_options();\n        let heading_item = |name, width, msg| {\n            let mut row = widget::row::with_capacity(2)\n                .align_y(Alignment::Center)\n                .spacing(space_xxxs)\n                .width(width);\n            row = row.push(widget::text::heading(name));\n            match (sort_name == msg, sort_direction) {\n                (true, true) => {\n                    row = row.push(widget::icon::from_name(\"pan-down-symbolic\").size(16));\n                }\n                (true, false) => {\n                    row = row.push(widget::icon::from_name(\"pan-up-symbolic\").size(16));\n                }\n                _ => {}\n            }\n            //TODO: make it possible to resize with the mouse\n            mouse_area::MouseArea::new(row)\n                .on_press(move |_point_opt| Message::ToggleSort(msg))\n                .into()\n        };\n\n        let heading_row = widget::row::with_children([\n            heading_item(fl!(\"name\"), Length::Fill, HeadingOptions::Name),\n            if self.location.is_trash() {\n                heading_item(\n                    fl!(\"trashed-on\"),\n                    Length::Fixed(modified_width),\n                    HeadingOptions::TrashedOn,\n                )\n            } else {\n                heading_item(\n                    fl!(\"modified\"),\n                    Length::Fixed(modified_width),\n                    HeadingOptions::Modified,\n                )\n            },\n            heading_item(fl!(\"size\"), Length::Fixed(size_width), HeadingOptions::Size),\n        ])\n        .align_y(Alignment::Center)\n        .height(Length::Fixed((space_m + 4).into()))\n        .padding([0, space_xxs]);\n\n        let accent_rule =\n            rule::horizontal(1).class(theme::Rule::Custom(Box::new(|theme| rule::Style {\n                color: theme.cosmic().accent_color().into(),\n                radius: 0.0.into(),\n                fill_mode: rule::FillMode::Full,\n                snap: true,\n            })));\n        let heading_rule = widget::container(rule::horizontal(1))\n            .padding([0, theme::active().cosmic().corner_radii.radius_xs[0] as u16]);\n\n        if let Some(edit_location) = &self.edit_location {\n            let mut text_input = None;\n\n            //TODO: allow editing other locations\n            if let Location::Network(ref uri, ..) = edit_location.location {\n                let location = edit_location.location.clone();\n                text_input = Some(\n                    widget::text_input(\"\", uri.clone())\n                        .id(self.edit_location_id.clone())\n                        .on_input(move |input| {\n                            Message::EditLocation(Some(location.with_uri(input).into()))\n                        })\n                        .on_submit(|_| Message::EditLocationSubmit)\n                        .line_height(1.0),\n                );\n            } else if let Some(resolved_location) = edit_location.resolve()\n                && let Some(path) = resolved_location.path_opt().cloned()\n            {\n                text_input = Some(\n                    widget::text_input(\"\", path.to_string_lossy().into_owned())\n                        .id(self.edit_location_id.clone())\n                        .on_input(move |input| {\n                            Message::EditLocation(Some(\n                                resolved_location.with_path(PathBuf::from(input)).into(),\n                            ))\n                        })\n                        .on_submit(|_| Message::EditLocationSubmit)\n                        .on_tab(Message::EditLocationTab)\n                        .on_unfocus(Message::EditLocation(None))\n                        .line_height(1.0),\n                );\n            }\n            if let Some(text_input) = text_input {\n                row = row.push(\n                    widget::button::custom(\n                        widget::icon::from_name(\"window-close-symbolic\").size(16),\n                    )\n                    .on_press(Message::EditLocation(None))\n                    .padding(space_xxs)\n                    .class(theme::Button::Icon),\n                );\n                let mut popover =\n                    widget::popover(text_input).position(widget::popover::Position::Bottom);\n                if let Some(completions) = &edit_location.completions\n                    && !completions.is_empty()\n                {\n                    let mut column =\n                        widget::column::with_capacity(completions.len()).padding(space_xxs);\n                    for (i, (name, _path)) in completions.iter().enumerate() {\n                        let selected = edit_location.selected == Some(i);\n                        column = column.push(\n                            widget::button::custom(widget::text::body(name))\n                                //TODO: match to design\n                                .class(if selected {\n                                    theme::Button::Standard\n                                } else {\n                                    theme::Button::HeaderBar\n                                })\n                                .on_press(Message::EditLocationComplete(i))\n                                .padding(space_xxs)\n                                .width(Length::Fill),\n                        );\n                    }\n                    popover = popover.popup(\n                        widget::container(column)\n                            .class(theme::Container::Dropdown)\n                            //TODO: This is a hack to get the popover to be the right width\n                            .max_width(size.width - 140.0),\n                    );\n                }\n                row = row.push(popover);\n                let mut column = widget::column::with_capacity(4).padding([0, space_s]);\n                column = column.push(row);\n                column = column.push(accent_rule);\n                if self.config.view == View::List && !condensed {\n                    column = column.push(heading_row);\n                    column = column.push(heading_rule);\n                }\n                return column.into();\n            }\n        } else if let Some(path) = self.location.path_opt() {\n            row = row.push(\n                crate::mouse_area::MouseArea::new(\n                    widget::button::custom(widget::icon::from_name(\"edit-symbolic\").size(16))\n                        .padding(space_xxs)\n                        .class(theme::Button::Icon)\n                        .on_press(Message::EditLocation(Some(self.location.clone().into()))),\n                )\n                .on_middle_press(move |_| Message::OpenInNewTab(path.clone())),\n            );\n            w += f32::from(space_xxs).mul_add(2.0, 16.0);\n        }\n\n        let mut children: Vec<Element<_>> = Vec::new();\n        match &self.location {\n            Location::Desktop(path, ..)\n            | Location::Path(path)\n            | Location::Search(SearchLocation::Path(path), ..) => {\n                let excess_str = \"...\";\n                let excess_width = text_width_body(excess_str);\n                for (index, ancestor) in path.ancestors().enumerate() {\n                    let (name, found_home) = folder_name(ancestor);\n                    let (name_width, name_text) = if children.is_empty() {\n                        (\n                            text_width_heading(&name),\n                            widget::text::heading(name)\n                                .wrapping(text::Wrapping::None)\n                                .ellipsize(text::Ellipsize::End(\n                                    text::EllipsizeHeightLimit::Lines(1),\n                                )),\n                        )\n                    } else {\n                        children.push(\n                            widget::icon::from_name(\"go-next-symbolic\")\n                                .size(16)\n                                .icon()\n                                .into(),\n                        );\n                        w += 16.0;\n                        (\n                            text_width_body(&name),\n                            widget::text::body(name).wrapping(text::Wrapping::None),\n                        )\n                    };\n\n                    // Add padding for mouse area\n                    w += 2.0 * f32::from(space_xxxs);\n\n                    let mut row = widget::row::with_capacity(2)\n                        .align_y(Alignment::Center)\n                        .spacing(space_xxxs);\n                    //TODO: figure out why this hardcoded offset is needed after the first item is ellipsed\n                    let overflow_offset = 64.0;\n                    let overflow = w + name_width + overflow_offset > size.width && index > 0;\n                    if overflow {\n                        row = row.push(widget::text::body(excess_str));\n                        w += excess_width;\n                    } else {\n                        row = row.push(name_text);\n                        w += name_width;\n                    }\n\n                    let location = self.location.with_path(ancestor.to_path_buf());\n                    let mut mouse_area = crate::mouse_area::MouseArea::new(\n                        widget::button::custom(row)\n                            .padding(space_xxxs)\n                            .class(theme::Button::Link)\n                            .on_press(if ancestor == path {\n                                Message::EditLocation(Some(self.location.clone().into()))\n                            } else {\n                                Message::Location(location.clone())\n                            }),\n                    );\n\n                    if self.location_context_menu_index.is_some() {\n                        mouse_area = mouse_area\n                            .on_right_press(move |point_opt| {\n                                Message::LocationContextMenuIndex(point_opt, None)\n                            })\n                            .wayland_on_right_press_window_position();\n                    } else {\n                        mouse_area = mouse_area\n                            .on_right_press_no_capture()\n                            .on_right_press(move |point_opt| {\n                                Message::LocationContextMenuIndex(point_opt, Some(index))\n                            })\n                            .wayland_on_right_press_window_position();\n                    }\n\n                    let mouse_area = if let Location::Path(_) = &self.location {\n                        mouse_area\n                            .on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf()))\n                    } else {\n                        mouse_area\n                    };\n\n                    children.push(self.dnd_dest(&location, mouse_area));\n\n                    if found_home || overflow {\n                        break;\n                    }\n                }\n                children.reverse();\n            }\n            Location::Trash | Location::Search(SearchLocation::Trash, ..) => {\n                children.push(\n                    widget::button::custom(widget::text::heading(fl!(\"trash\")))\n                        .padding(space_xxxs)\n                        .on_press(Message::Location(Location::Trash))\n                        .class(theme::Button::Text)\n                        .into(),\n                );\n            }\n            Location::Recents | Location::Search(SearchLocation::Recents, ..) => {\n                children.push(\n                    widget::button::custom(widget::text::heading(fl!(\"recents\")))\n                        .padding(space_xxxs)\n                        .on_press(Message::Location(Location::Recents))\n                        .class(theme::Button::Text)\n                        .into(),\n                );\n            }\n            Location::Network(uri, display_name, path) => {\n                children.push(\n                    widget::button::custom(widget::text::heading(display_name))\n                        .padding(space_xxxs)\n                        .on_press(Message::Location(Location::Network(\n                            uri.clone(),\n                            display_name.clone(),\n                            path.clone(),\n                        )))\n                        .class(theme::Button::Text)\n                        .into(),\n                );\n            }\n        }\n\n        row = row.extend(children);\n        let mut column = widget::column::with_capacity(4).padding([0, space_s]);\n        column = column.push(row);\n        column = column.push(accent_rule);\n\n        if self.config.view == View::List && !condensed {\n            column = column.push(heading_row);\n            column = column.push(heading_rule);\n        }\n\n        let mouse_area = crate::mouse_area::MouseArea::new(column)\n            .on_right_press(Message::LocationContextMenuPoint);\n\n        let mut popover = widget::popover(mouse_area);\n        if let (Some(point), Some(index)) = (\n            self.location_context_menu_point,\n            self.location_context_menu_index,\n        ) {\n            popover = popover\n                .popup(menu::location_context_menu(index))\n                .position(widget::popover::Position::Point(point));\n        }\n\n        popover.into()\n    }\n\n    pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {\n        let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();\n\n        mouse_area::MouseArea::new(widget::column::with_children([widget::container(\n            match self.mode {\n                Mode::App | Mode::Dialog(_) => widget::column::with_children([\n                    widget::icon::from_name(\"folder-symbolic\")\n                        .size(64)\n                        .icon()\n                        .into(),\n                    widget::text::body(if has_hidden {\n                        fl!(\"empty-folder-hidden\")\n                    } else if matches!(self.location, Location::Search(..)) {\n                        fl!(\"no-results\")\n                    } else {\n                        fl!(\"empty-folder\")\n                    })\n                    .into(),\n                ]),\n                Mode::Desktop => widget::column::with_capacity(0),\n            }\n            .align_x(Alignment::Center)\n            .spacing(space_xxs),\n        )\n        .center(Length::Fill)\n        .into()]))\n        .on_press(|_| Message::Click(None))\n        .into()\n    }\n\n    pub fn grid_view(\n        &self,\n    ) -> (\n        Option<Element<'static, Message>>,\n        Element<'_, Message>,\n        bool,\n    ) {\n        let cosmic_theme::Spacing {\n            space_xxs,\n            space_xxxs,\n            ..\n        } = theme::spacing();\n\n        let TabConfig {\n            show_hidden,\n            mut icon_sizes,\n            ..\n        } = self.config;\n\n        let mut grid_spacing = space_xxs;\n        if let Location::Desktop(_path, _output, desktop_config) = &self.location {\n            icon_sizes.grid = desktop_config.icon_size;\n            grid_spacing = desktop_config.grid_spacing_for(space_xxs);\n        }\n\n        let text_height = 3 * 20; // 3 lines of text\n        let item_width = (3 * space_xxs + icon_sizes.grid() + 3 * space_xxs) as usize;\n        let item_height =\n            (space_xxxs + icon_sizes.grid() + space_xxxs + text_height + space_xxxs) as usize;\n\n        let (width, height) = match self.size_opt.get() {\n            Some(size) => (\n                (size.width.floor() as usize)\n                    .saturating_sub(2 * (space_xxs as usize))\n                    .max(item_width),\n                (size.height.floor() as usize).max(item_height),\n            ),\n            None => (item_width, item_height),\n        };\n\n        let (cols, column_spacing) = {\n            let width_m1 = width.saturating_sub(item_width);\n            let cols_m1 = width_m1 / (item_width + grid_spacing as usize);\n            let cols = cols_m1 + 1;\n            let spacing = width_m1\n                .checked_div(cols_m1)\n                .unwrap_or(0)\n                .saturating_sub(item_width);\n            (cols, spacing as u16)\n        };\n\n        let rows = {\n            let height_m1 = height.saturating_sub(item_height);\n            let rows_m1 = height_m1 / (item_height + grid_spacing as usize);\n            rows_m1 + 1\n        };\n\n        //TODO: move to function\n        let visible_rect = {\n            // Use cached content height to clamp scroll offset after resize\n            let max_scroll_y = self\n                .content_height_opt\n                .get()\n                .map(|ch| (ch - height as f32).max(0.0))\n                .unwrap_or(f32::MAX);\n            let scroll_y = self\n                .scroll_opt\n                .map(|o| o.y.min(max_scroll_y).max(0.0))\n                .unwrap_or(0.0);\n            let point = Point::new(0.0, scroll_y);\n            let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));\n            Rectangle::new(point, size)\n        };\n\n        let mut grid = widget::grid()\n            .column_spacing(column_spacing)\n            .row_spacing(grid_spacing)\n            .padding(space_xxs.into());\n        let mut dnd_items: Vec<(usize, (usize, usize), &Item)> = Vec::new();\n        let mut drag_w_i = usize::MAX;\n        let mut drag_n_i = usize::MAX;\n        let mut drag_e_i = 0;\n        let mut drag_s_i = 0;\n\n        let mut column = widget::column::with_capacity(2);\n        if let Some(items) = self.column_sort() {\n            let mut count = 0;\n            let mut col = 0;\n            let mut row = 0;\n            let mut page_row = 0;\n            let mut hidden = 0;\n            let mut grid_elements = Vec::new();\n            for &(i, item) in &items {\n                if !show_hidden && item.hidden {\n                    item.pos_opt.set(None);\n                    item.rect_opt.set(None);\n                    hidden += 1;\n                    continue;\n                }\n                item.pos_opt.set(Some((row, col)));\n                let item_rect = Rectangle::new(\n                    Point::new(\n                        (col * (item_width + column_spacing as usize) + space_xxs as usize) as f32,\n                        (row * (item_height + grid_spacing as usize) + space_xxs as usize) as f32,\n                    ),\n                    Size::new(item_width as f32, item_height as f32),\n                );\n                item.rect_opt.set(Some(item_rect));\n\n                //TODO: error if the row or col is already set?\n                while grid_elements.len() <= row {\n                    grid_elements.push(Vec::new());\n                }\n\n                // Only build elements if visible (for performance)\n                if item_rect.intersects(&visible_rect) {\n                    //TODO: one focus group per grid item (needs custom widget)\n                    let buttons: Vec<Element<Message>> = vec![\n                        widget::button::custom(\n                            widget::icon::icon(item.icon_handle_grid.clone())\n                                .content_fit(ContentFit::Contain)\n                                .size(icon_sizes.grid())\n                                .width(Length::Shrink),\n                        )\n                        .padding(space_xxxs)\n                        .class(button_style(\n                            item.selected,\n                            item.highlighted,\n                            item.cut,\n                            false,\n                            false,\n                            false,\n                        ))\n                        .into(),\n                        widget::tooltip(\n                            widget::button::custom(Item::grid_display_name(&item.display_name))\n                                .id(item.button_id.clone())\n                                .padding([0, space_xxxs])\n                                .class(button_style(\n                                    item.selected,\n                                    item.highlighted,\n                                    item.cut,\n                                    true,\n                                    true,\n                                    matches!(self.mode, Mode::Desktop),\n                                )),\n                            widget::text::body(&item.name),\n                            widget::tooltip::Position::Bottom,\n                        )\n                        .into(),\n                    ];\n\n                    let mut column = widget::column::with_capacity(buttons.len())\n                        .align_x(Alignment::Center)\n                        .height(Length::Fixed(item_height as f32))\n                        .width(Length::Fixed(item_width as f32));\n                    for button in buttons {\n                        if self.context_menu.is_some() {\n                            column = column.push(button);\n                        } else {\n                            column = column.push(\n                                mouse_area::MouseArea::new(button)\n                                    .on_right_press_no_capture()\n                                    .wayland_on_right_press_window_position()\n                                    .on_right_press(move |point_opt| {\n                                        Message::RightClick(point_opt, Some(i))\n                                    }),\n                            );\n                        }\n                    }\n\n                    let column: Element<Message> =\n                        if item.metadata.is_dir() && item.location_opt.is_some() {\n                            self.dnd_dest(&item.location_opt.clone().unwrap(), column)\n                        } else {\n                            column.into()\n                        };\n\n                    if item.selected {\n                        dnd_items.push((i, (row, col), item));\n                        drag_w_i = drag_w_i.min(col);\n                        drag_n_i = drag_n_i.min(row);\n                        drag_e_i = drag_e_i.max(col);\n                        drag_s_i = drag_s_i.max(row);\n                    }\n                    let mouse_area = crate::mouse_area::MouseArea::new(column)\n                        .on_press(move |_| Message::Click(Some(i)))\n                        .on_double_click(move |_| Message::DoubleClick(Some(i)))\n                        .on_release(move |_| Message::ClickRelease(Some(i)))\n                        .on_middle_press(move |_| Message::MiddleClick(i))\n                        .on_enter(move || Message::HighlightActivate(i))\n                        .on_exit(move || Message::HighlightDeactivate(i));\n                    grid_elements[row].push(Element::from(mouse_area));\n                } else {\n                    // Add a spacer if the row is empty, so scroll works\n                    if grid_elements[row].is_empty() {\n                        grid_elements[row].push(Element::from(\n                            widget::column::with_capacity(0)\n                                .width(Length::Fill)\n                                .height(Length::Fixed(item_height as f32)),\n                        ));\n                    }\n                }\n\n                count += 1;\n                if matches!(self.mode, Mode::Desktop) {\n                    row += 1;\n                    if row >= page_row + rows {\n                        row = 0;\n                        col += 1;\n                    }\n                    if col >= cols {\n                        col = 0;\n                        page_row += rows;\n                        row = page_row;\n                    }\n                } else {\n                    col += 1;\n                    if col >= cols {\n                        col = 0;\n                        row += 1;\n                    }\n                }\n            }\n\n            for row_elements in grid_elements {\n                for element in row_elements {\n                    grid = grid.push(element);\n                }\n                grid = grid.insert_row();\n            }\n\n            if count == 0 {\n                return (None, self.empty_view(hidden > 0), false);\n            }\n\n            column = column.push(grid);\n\n            //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that\n            {\n                let mut max_bottom = 0;\n                for (_, item) in items {\n                    if let Some(rect) = item.rect_opt.get() {\n                        let bottom = (rect.y + rect.height).ceil() as usize;\n                        if bottom > max_bottom {\n                            max_bottom = bottom;\n                        }\n                    }\n                }\n\n                // Cache content height for scroll clamping on next frame\n                self.content_height_opt.set(Some(max_bottom as f32));\n\n                let top_deduct = 7 * (space_xxs as usize);\n\n                self.item_view_size_opt\n                    .set(self.size_opt.get().map(|s| Size {\n                        width: s.width,\n                        height: s.height - top_deduct as f32,\n                    }));\n\n                let spacer_height = height.saturating_sub(max_bottom + top_deduct);\n                if spacer_height > 0 {\n                    column = column.push(widget::container(\n                        space::vertical().height(Length::Fixed(spacer_height as f32)),\n                    ));\n                }\n            }\n        }\n\n        let drag_list = (!dnd_items.is_empty()).then(|| {\n            let mut dnd_grid = widget::grid()\n                .column_spacing(column_spacing)\n                .row_spacing(grid_spacing)\n                .padding(space_xxs.into());\n\n            let mut dnd_item_i = 0;\n            for r in drag_n_i..=drag_s_i {\n                dnd_grid = dnd_grid.insert_row();\n                for c in drag_w_i..=drag_e_i {\n                    let Some((i, (row, col), item)) = dnd_items.get(dnd_item_i) else {\n                        break;\n                    };\n                    if *row == r && *col == c {\n                        let buttons = vec![\n                            widget::button::custom(\n                                widget::icon::icon(item.icon_handle_grid.clone())\n                                    .content_fit(ContentFit::Contain)\n                                    .size(icon_sizes.grid()),\n                            )\n                            .on_press(Message::Click(Some(*i)))\n                            .padding(space_xxxs)\n                            .class(button_style(\n                                item.selected,\n                                item.highlighted,\n                                item.cut,\n                                false,\n                                false,\n                                false,\n                            )),\n                            widget::button::custom(Item::grid_display_name(\n                                item.display_name.clone(),\n                            ))\n                            .id(item.button_id.clone())\n                            .on_press(Message::Click(Some(*i)))\n                            .padding([0, space_xxxs])\n                            .class(button_style(\n                                item.selected,\n                                item.highlighted,\n                                item.cut,\n                                true,\n                                true,\n                                false,\n                            )),\n                        ];\n\n                        let column =\n                            widget::column::with_children(buttons.into_iter().map(Element::from))\n                                .align_x(Alignment::Center)\n                                .height(Length::Fixed(item_height as f32))\n                                .width(Length::Fixed(item_width as f32));\n\n                        dnd_grid = dnd_grid.push(column);\n                        dnd_item_i += 1;\n                    } else {\n                        dnd_grid = dnd_grid.push(\n                            widget::container(space::vertical().height(item_width as f32))\n                                .height(Length::Fixed(item_height as f32)),\n                        );\n                    }\n                }\n            }\n            Element::from(dnd_grid)\n        });\n\n        let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill))\n            .on_press(|_| Message::Click(None))\n            .on_auto_scroll(Message::AutoScroll)\n            .on_drag_end(|_| Message::DragEnd)\n            .show_drag_rect(self.mode.multiple())\n            .on_release(|_| Message::ClickRelease(None));\n        if self.watch_drag {\n            mouse_area = mouse_area.on_drag(Message::Drag);\n        }\n\n        (drag_list, mouse_area.into(), true)\n    }\n\n    pub fn list_view(\n        &self,\n    ) -> (\n        Option<Element<'static, Message>>,\n        Element<'_, Message>,\n        bool,\n    ) {\n        let cosmic_theme::Spacing {\n            space_s, space_xxs, ..\n        } = theme::spacing();\n\n        let TabConfig {\n            show_hidden,\n            icon_sizes,\n            ..\n        } = self.config;\n\n        let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));\n        //TODO: allow resizing?\n        let name_width = 300.0;\n        let modified_width = 200.0;\n        let size_width = 100.0;\n        let condensed = size.width < (name_width + modified_width + size_width);\n        let is_search = matches!(self.location, Location::Search(..));\n        let icon_size = if condensed || is_search {\n            icon_sizes.list_condensed()\n        } else {\n            icon_sizes.list()\n        };\n        let row_height = icon_size + 2 * space_xxs;\n\n        let mut column = widget::column::with_capacity(3);\n        let mut y: f32 = 0.0;\n\n        let rule_padding = theme::active().cosmic().corner_radii.radius_xs[0] as u16;\n\n        //TODO: move to function\n        let visible_rect = {\n            // Use cached content height to clamp scroll offset after resize\n            let max_scroll_y = self\n                .content_height_opt\n                .get()\n                .map(|ch| (ch - size.height).max(0.0))\n                .unwrap_or(f32::MAX);\n            let scroll_y = self\n                .scroll_opt\n                .map(|o| o.y.min(max_scroll_y).max(0.0))\n                .unwrap_or(0.0);\n            let point = Point::new(0.0, scroll_y);\n            let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));\n            Rectangle::new(point, size)\n        };\n\n        let mut drag_items = Vec::new();\n        if let Some(items) = self.column_sort() {\n            let mut count = 0;\n            let mut hidden = 0;\n            for (i, item) in items {\n                if item.hidden && !show_hidden {\n                    item.pos_opt.set(None);\n                    item.rect_opt.set(None);\n                    hidden += 1;\n                    continue;\n                }\n\n                if count > 0 {\n                    column = column\n                        .push(widget::container(rule::horizontal(1)).padding([0, rule_padding]));\n                    y += 1.0;\n                }\n\n                item.pos_opt.set(Some((count, 0)));\n                let item_rect = Rectangle::new(\n                    Point::new(f32::from(space_s), y),\n                    Size::new(size.width - f32::from(2 * space_s), f32::from(row_height)),\n                );\n                item.rect_opt.set(Some(item_rect));\n\n                // Only build elements if visible (for performance)\n                let button_row = if item_rect.intersects(&visible_rect) {\n                    let modified_text = match &item.metadata {\n                        ItemMetadata::Path { metadata, .. } => match metadata.modified() {\n                            Ok(time) => self.format_time(time).to_string(),\n                            Err(_) => String::new(),\n                        },\n                        ItemMetadata::Trash { entry, .. } => FormatTime::from_secs(\n                            entry.time_deleted,\n                            &self.date_time_formatter,\n                            &self.time_formatter,\n                        )\n                        .map(|t| t.to_string())\n                        .unwrap_or_default(),\n                        #[cfg(feature = \"gvfs\")]\n                        ItemMetadata::GvfsPath { .. } => match item.metadata.modified() {\n                            Some(mtime) => self.format_time(mtime).to_string(),\n                            None => String::new(),\n                        },\n                        _ => String::new(),\n                    };\n\n                    let size_text = match &item.metadata {\n                        ItemMetadata::Path {\n                            metadata,\n                            children_opt,\n                        } => {\n                            if metadata.is_dir() {\n                                //TODO: translate\n                                if let Some(children) = children_opt {\n                                    if *children == 1 {\n                                        format!(\"{children} item\")\n                                    } else {\n                                        format!(\"{children} items\")\n                                    }\n                                } else {\n                                    String::new()\n                                }\n                            } else {\n                                format_size(metadata.len())\n                            }\n                        }\n                        ItemMetadata::Trash { metadata, .. } => match metadata.size {\n                            trash::TrashItemSize::Entries(entries) => {\n                                //TODO: translate\n                                if entries == 1 {\n                                    format!(\"{entries} item\")\n                                } else {\n                                    format!(\"{entries} items\")\n                                }\n                            }\n                            trash::TrashItemSize::Bytes(bytes) => format_size(bytes),\n                        },\n                        ItemMetadata::SimpleDir { entries } => {\n                            //TODO: translate\n                            if *entries == 1 {\n                                format!(\"{entries} item\")\n                            } else {\n                                format!(\"{entries} items\")\n                            }\n                        }\n                        ItemMetadata::SimpleFile { size } => format_size(*size),\n                        #[cfg(feature = \"gvfs\")]\n                        ItemMetadata::GvfsPath {\n                            size_opt,\n                            children_opt,\n                            ..\n                        } => match children_opt {\n                            Some(child_count) => {\n                                if *child_count == 1 {\n                                    format!(\"{child_count} item\")\n                                } else {\n                                    format!(\"{child_count} items\")\n                                }\n                            }\n                            None => format_size(size_opt.unwrap_or_default()),\n                        },\n                    };\n\n                    let row = if condensed {\n                        widget::row::with_children([\n                            widget::icon::icon(item.icon_handle_list_condensed.clone())\n                                .content_fit(ContentFit::Contain)\n                                .size(icon_size)\n                                .into(),\n                            widget::column::with_children([\n                                Item::list_display_name(item.display_name.clone()).into(),\n                                //TODO: translate?\n                                widget::text::caption(format!(\"{modified_text} - {size_text}\"))\n                                    .into(),\n                            ])\n                            .into(),\n                        ])\n                        .height(Length::Fixed(f32::from(row_height)))\n                        .align_y(Alignment::Center)\n                        .spacing(space_xxs)\n                    } else if is_search {\n                        widget::row::with_children([\n                            widget::icon::icon(item.icon_handle_list_condensed.clone())\n                                .content_fit(ContentFit::Contain)\n                                .size(icon_size)\n                                .into(),\n                            widget::column::with_children([\n                                Item::list_display_name(item.display_name.clone()).into(),\n                                widget::text::caption(match item.path_opt() {\n                                    Some(path) => path.display().to_string(),\n                                    None => String::new(),\n                                })\n                                .into(),\n                            ])\n                            .width(Length::Fill)\n                            .into(),\n                            widget::text::body(modified_text.clone())\n                                .width(Length::Fixed(modified_width))\n                                .into(),\n                            widget::text::body(size_text.clone())\n                                .width(Length::Fixed(size_width))\n                                .into(),\n                        ])\n                        .height(Length::Fixed(f32::from(row_height)))\n                        .align_y(Alignment::Center)\n                        .spacing(space_xxs)\n                    } else {\n                        widget::row::with_children([\n                            widget::icon::icon(item.icon_handle_list.clone())\n                                .content_fit(ContentFit::Contain)\n                                .size(icon_size)\n                                .into(),\n                            Item::list_display_name(item.display_name.clone())\n                                .width(Length::Fill)\n                                .into(),\n                            widget::text::body(modified_text.clone())\n                                .width(Length::Fixed(modified_width))\n                                .into(),\n                            widget::text::body(size_text.clone())\n                                .width(Length::Fixed(size_width))\n                                .into(),\n                        ])\n                        .height(Length::Fixed(f32::from(row_height)))\n                        .align_y(Alignment::Center)\n                        .spacing(space_xxs)\n                    };\n\n                    let button = |row| {\n                        let mouse_area = crate::mouse_area::MouseArea::new(\n                            widget::button::custom(row)\n                                .width(Length::Fill)\n                                .id(item.button_id.clone())\n                                .padding([0, space_xxs])\n                                .class(button_style(\n                                    item.selected,\n                                    item.highlighted,\n                                    item.cut,\n                                    true,\n                                    true,\n                                    false,\n                                )),\n                        )\n                        .on_press(move |_| Message::Click(Some(i)))\n                        .on_double_click(move |_| Message::DoubleClick(Some(i)))\n                        .on_release(move |_| Message::ClickRelease(Some(i)))\n                        .on_middle_press(move |_| Message::MiddleClick(i))\n                        .on_enter(move || Message::HighlightActivate(i))\n                        .on_exit(move || Message::HighlightDeactivate(i));\n\n                        if self.context_menu.is_some() {\n                            mouse_area\n                        } else {\n                            mouse_area\n                                .on_right_press_no_capture()\n                                .wayland_on_right_press_window_position()\n                                .on_right_press(move |point_opt| {\n                                    Message::RightClick(point_opt, Some(i))\n                                })\n                        }\n                    };\n\n                    let button_row = button(row.into());\n                    let button_row: Element<_> = if item.metadata.is_dir()\n                        && let Some(location) = item.location_opt.as_ref()\n                    {\n                        self.dnd_dest(location, button_row)\n                    } else {\n                        button_row.into()\n                    };\n\n                    if item.selected || !drag_items.is_empty() {\n                        let dnd_row = if !item.selected {\n                            Element::from(\n                                space::vertical().height(Length::Fixed(f32::from(row_height))),\n                            )\n                        } else if condensed {\n                            widget::row::with_children([\n                                widget::icon::icon(item.icon_handle_list_condensed.clone())\n                                    .content_fit(ContentFit::Contain)\n                                    .size(icon_size)\n                                    .into(),\n                                widget::column::with_children([\n                                    Item::list_display_name(item.display_name.clone()).into(),\n                                    //TODO: translate?\n                                    widget::text::body(format!(\"{modified_text} - {size_text}\"))\n                                        .into(),\n                                ])\n                                .into(),\n                            ])\n                            .align_y(Alignment::Center)\n                            .spacing(space_xxs)\n                            .into()\n                        } else if is_search {\n                            widget::row::with_children([\n                                widget::icon::icon(item.icon_handle_list_condensed.clone())\n                                    .content_fit(ContentFit::Contain)\n                                    .size(icon_size)\n                                    .into(),\n                                widget::column::with_children([\n                                    Item::list_display_name(item.display_name.clone()).into(),\n                                    widget::text::caption(match item.path_opt() {\n                                        Some(path) => path.display().to_string(),\n                                        None => String::new(),\n                                    })\n                                    .into(),\n                                ])\n                                .width(Length::Fill)\n                                .into(),\n                                widget::text::body(modified_text.clone())\n                                    .width(Length::Fixed(modified_width))\n                                    .into(),\n                                widget::text::body(size_text.clone())\n                                    .width(Length::Fixed(size_width))\n                                    .into(),\n                            ])\n                            .align_y(Alignment::Center)\n                            .spacing(space_xxs)\n                            .into()\n                        } else {\n                            widget::row::with_children([\n                                widget::icon::icon(item.icon_handle_list.clone())\n                                    .content_fit(ContentFit::Contain)\n                                    .size(icon_size)\n                                    .into(),\n                                Item::list_display_name(item.display_name.clone())\n                                    .width(Length::Fill)\n                                    .into(),\n                                widget::text(modified_text)\n                                    .width(Length::Fixed(modified_width))\n                                    .into(),\n                                widget::text::body(size_text)\n                                    .width(Length::Fixed(size_width))\n                                    .into(),\n                            ])\n                            .align_y(Alignment::Center)\n                            .spacing(space_xxs)\n                            .into()\n                        };\n                        if item.selected {\n                            drag_items.push(\n                                widget::container(button(dnd_row))\n                                    .width(Length::Shrink)\n                                    .into(),\n                            );\n                        } else {\n                            drag_items.push(dnd_row);\n                        }\n                    }\n\n                    button_row\n                } else {\n                    widget::column::with_capacity(0)\n                        .width(Length::Fill)\n                        .height(Length::Fixed(f32::from(row_height)))\n                        .into()\n                };\n\n                count += 1;\n                y += f32::from(row_height);\n                column = column.push(button_row);\n            }\n\n            if count == 0 {\n                return (None, self.empty_view(hidden > 0), false);\n            }\n\n            // Cache content height for scroll clamping on next frame\n            self.content_height_opt.set(Some(y));\n        }\n        //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that\n        {\n            let top_deduct = (if condensed || is_search { 6 } else { 9 }) * space_xxs;\n\n            self.item_view_size_opt\n                .set(self.size_opt.get().map(|s| Size {\n                    width: s.width,\n                    height: s.height - f32::from(top_deduct),\n                }));\n\n            let spacer_height = size.height - y - f32::from(top_deduct);\n            if spacer_height > 0. {\n                column = column.push(widget::container(space::vertical().height(spacer_height)));\n            }\n        }\n        let drag_col = (!drag_items.is_empty())\n            .then(|| Element::from(widget::column::with_children(drag_items)));\n\n        let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s]))\n            .with_id(Id::new(\"list-view\"))\n            .on_press(|_| Message::Click(None))\n            .on_auto_scroll(Message::AutoScroll)\n            .on_drag_end(|_| Message::DragEnd)\n            .show_drag_rect(self.mode.multiple())\n            .on_release(|_| Message::ClickRelease(None));\n        if self.watch_drag {\n            mouse_area = mouse_area.on_drag(Message::Drag);\n        }\n\n        (drag_col, mouse_area.into(), true)\n    }\n\n    pub fn view_responsive<'a>(\n        &'a self,\n        key_binds: &'a HashMap<KeyBind, Action>,\n        modifiers: &'a Modifiers,\n        size: Size,\n        clipboard_paste_available: bool,\n        context_actions: &'a [ContextActionPreset],\n    ) -> Element<'a, Message> {\n        // Update cached size\n        self.size_opt.set(Some(size));\n\n        let cosmic_theme::Spacing {\n            space_xxxs,\n            space_xxs,\n            space_xs,\n            ..\n        } = theme::spacing();\n\n        let location_view_opt = if matches!(self.mode, Mode::Desktop) {\n            None\n        } else {\n            Some(self.location_view())\n        };\n        let (drag_list, mut item_view, can_scroll) = match self.config.view {\n            View::Grid => self.grid_view(),\n            View::List => self.list_view(),\n        };\n        item_view = widget::container(item_view).width(Length::Fill).into();\n        let files = self\n            .items_opt\n            .as_ref()\n            .map(|items| {\n                items\n                    .iter()\n                    .filter_map(|item| {\n                        if item.selected {\n                            item.path_opt().cloned()\n                        } else {\n                            None\n                        }\n                    })\n                    .collect::<Box<[PathBuf]>>()\n            })\n            .unwrap_or_default();\n        let item_view =\n            DndSource::<Message, ClipboardCopy>::with_id(item_view, Id::new(\"tab-view\"));\n\n        let view = self.config.view;\n        let item_view = match drag_list {\n            Some(drag_list) if self.selected_clicked => {\n                let drag_list = RcElementWrapper::new(drag_list);\n                item_view\n                    .drag_content(move || {\n                        ClipboardCopy::new(crate::clipboard::ClipboardKind::Copy, &files)\n                    })\n                    .drag_icon(move |_| {\n                        let state: tree::State = Widget::<Message, _, _>::state(&drag_list);\n                        (\n                            Element::from(drag_list.clone()).map(|_m| ()),\n                            state,\n                            match view {\n                                // offset by grid padding so that we grab the top left corner of the item in the drag grid.\n                                View::Grid => Vector::new(\n                                    f32::from(space_xxs).mul_add(-3.0, -f32::from(space_xxxs)),\n                                    -4. * f32::from(space_xxxs),\n                                ),\n                                View::List => Vector::ZERO,\n                            },\n                        )\n                    })\n            }\n            _ => item_view,\n        };\n\n        let tab_location = self.location.clone();\n        let mouse_area = mouse_area::MouseArea::new(item_view)\n            .on_press(move |_point_opt| Message::Click(None))\n            .on_release(|_| Message::ClickRelease(None))\n            .on_resize(Message::Resize)\n            .on_back_press(move |_point_opt| Message::GoPrevious)\n            .on_forward_press(move |_point_opt| Message::GoNext)\n            .on_scroll(|delta| respond_to_scroll_direction(delta, modifiers))\n            .on_right_press(move |p| {\n                Message::ContextMenu(\n                    if self.context_menu.is_some() { None } else { p },\n                    self.window_id,\n                )\n            })\n            .wayland_on_right_press_window_position();\n\n        let mut popover = widget::popover(mouse_area);\n        if let Some(point) = self.context_menu\n            && (!cfg!(feature = \"wayland\") || !crate::is_wayland())\n        {\n            let context_menu = menu::context_menu(\n                self,\n                key_binds,\n                modifiers,\n                clipboard_paste_available,\n                context_actions,\n            );\n            popover = popover\n                .popup(context_menu)\n                .position(widget::popover::Position::Point(point));\n        }\n\n        let mut tab_column = widget::column::with_capacity(3);\n        if let Some(location_view) = location_view_opt {\n            tab_column = tab_column.push(location_view);\n        }\n        if can_scroll {\n            tab_column = tab_column.push(\n                // FIXME: new responsive widget will remove the state from the scrollable\n                // id_container with custom id forces the state to be extracted in a diff\n                // pre-processing step\n                widget::id_container(\n                    widget::scrollable(popover)\n                        .id(self.scrollable_id.clone())\n                        .on_scroll(Message::Scroll)\n                        .width(Length::Fill)\n                        .height(Length::Fill),\n                    widget::Id::new(format!(\"{}-scrollable\", self.scrollable_id)),\n                ),\n            );\n        } else {\n            tab_column = tab_column.push(popover);\n        }\n        match &self.location {\n            Location::Trash | Location::Search(SearchLocation::Trash, ..) => {\n                if let Some(items) = self.items_opt()\n                    && !items.is_empty()\n                {\n                    tab_column = tab_column.push(\n                        widget::layer_container(widget::row::with_children([\n                            widget::space::horizontal().into(),\n                            widget::button::standard(fl!(\"empty-trash\"))\n                                .on_press(Message::EmptyTrash)\n                                .into(),\n                        ]))\n                        .padding([space_xxs, space_xs])\n                        .layer(cosmic_theme::Layer::Primary)\n                        .apply(widget::container)\n                        .padding([0, 0, 7, 0]),\n                    );\n                }\n            }\n            Location::Network(uri, _display_name, _path) if uri == \"network:///\" => {\n                tab_column = tab_column.push(\n                    widget::layer_container(widget::row::with_children([\n                        widget::space::horizontal().into(),\n                        widget::button::standard(fl!(\"add-network-drive\"))\n                            .on_press(Message::AddNetworkDrive)\n                            .into(),\n                    ]))\n                    .padding([space_xxs, space_xs])\n                    .layer(cosmic_theme::Layer::Primary)\n                    .apply(widget::container)\n                    .padding([0, 0, 7, 0]),\n                );\n            }\n            _ => {}\n        }\n        let mut tab_view = widget::container(tab_column)\n            .height(Length::Fill)\n            .width(Length::Fill);\n\n        // Desktop will not show DnD indicator\n        if self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location)\n            && !matches!(self.mode, Mode::Desktop)\n        {\n            tab_view = tab_view.style(|t| {\n                let mut a = widget::container::Style::default();\n                let c = t.cosmic();\n                a.border = cosmic::iced::core::Border {\n                    color: (c.accent_color()).into(),\n                    width: 1.,\n                    radius: c.radius_0().into(),\n                };\n                a\n            });\n        }\n\n        let tab_location_2 = self.location.clone();\n        let tab_location_3 = self.location.clone();\n        let dnd_dest = DndDestination::for_data(tab_view, move |data, action| {\n            if let Some(mut data) = data {\n                if action == DndAction::Copy {\n                    Message::Drop(Some((tab_location.clone(), data)))\n                } else if action == DndAction::Move {\n                    data.kind = ClipboardKind::Cut { is_dnd: true };\n                    Message::Drop(Some((tab_location.clone(), data)))\n                } else {\n                    log::warn!(\"unsupported action: {action:?}\");\n                    Message::Drop(None)\n                }\n            } else {\n                Message::Drop(None)\n            }\n        })\n        .on_enter(move |_, _, _| Message::DndEnter(tab_location_2.clone()))\n        .on_leave(move || Message::DndLeave(tab_location_3.clone()));\n\n        dnd_dest.into()\n    }\n    pub fn multi_preview_view<'a>(\n        &'a self,\n        mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>,\n    ) -> Element<'a, Message> {\n        let cosmic_theme::Spacing {\n            space_xxxs,\n            space_m,\n            ..\n        } = theme::spacing();\n\n        let mut column = widget::column::with_capacity(4).spacing(space_m);\n\n        let handle = widget::icon::from_name(\"text-x-generic\")\n            .size(IconSizes::default().grid())\n            .handle();\n\n        let icon = widget::icon::icon(handle.clone())\n            .content_fit(ContentFit::Contain)\n            .size(IconSizes::default().grid());\n\n        let icon_container1 = widget::container(icon.clone()).padding(padding::bottom(10).left(10));\n        let icon_container2 =\n            widget::container(icon.clone()).padding(padding::top(5).bottom(5).left(5).right(5));\n        let icon_container3 = widget::container(icon).padding(padding::top(10).right(10));\n        let stack = stack![icon_container1, icon_container2, icon_container3];\n\n        column = column.push(\n            widget::container(stack)\n                .center_x(Length::Fill)\n                .max_height(THUMBNAIL_SIZE as f32),\n        );\n\n        let selected_items: Vec<&Item> = self.items_opt().map_or(Vec::new(), |items| {\n            items\n                .iter()\n                .filter(|item| {\n                    if item.selected {\n                        item.location_opt\n                            .as_ref()\n                            .and_then(Location::path_opt)\n                            .is_some()\n                    } else {\n                        false\n                    }\n                })\n                .collect()\n        });\n\n        let mut details = widget::column::with_capacity(3).spacing(space_xxxs);\n        details = details.push(widget::text::body(fl!(\n            \"items\",\n            items = selected_items.len()\n        )));\n\n        let mut total_size: u64 = 0;\n        let mut mime_type_counts: BTreeMap<String, u64> = BTreeMap::new();\n        let mut user_name: BTreeSet<String> = BTreeSet::new();\n        let mut mode_user: BTreeSet<u32> = BTreeSet::new();\n        let mut group_name: BTreeSet<String> = BTreeSet::new();\n        let mut mode_group: BTreeSet<u32> = BTreeSet::new();\n        let mut mode_other: BTreeSet<u32> = BTreeSet::new();\n        let mut calculating_dir_size = false;\n        let mut dir_size_error: Option<String> = None;\n\n        for item in selected_items.iter() {\n            *mime_type_counts.entry(item.mime.to_string()).or_insert(0) += 1;\n\n            if let Some(metadata) = item.file_metadata() {\n                if metadata.is_dir() {\n                    match &item.dir_size {\n                        DirSize::Calculating(_) => {\n                            calculating_dir_size = true;\n                        }\n                        DirSize::Directory(size) => {\n                            total_size = total_size.saturating_add(*size);\n                        }\n                        DirSize::NotDirectory => (),\n                        DirSize::Error(err) => {\n                            dir_size_error = Some(err.clone());\n                        }\n                    };\n                } else {\n                    total_size = total_size.saturating_add(metadata.len());\n                }\n                #[cfg(unix)]\n                {\n                    let mode = metadata.mode();\n                    user_name.insert(\n                        uzers::get_user_by_uid(metadata.uid())\n                            .and_then(|user| user.name().to_str().map(ToOwned::to_owned))\n                            .unwrap_or_default(),\n                    );\n                    mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER));\n                    group_name.insert(\n                        uzers::get_group_by_gid(metadata.gid())\n                            .and_then(|group| group.name().to_str().map(ToOwned::to_owned))\n                            .unwrap_or_default(),\n                    );\n                    mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP));\n                    mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER));\n                }\n            }\n        }\n        let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect();\n        mime_types.sort_by(|(_, v1), (_, v2)| v2.cmp(v1));\n\n        // Limit the number of displayed mime types\n        let limit = usize::min(10, mime_types.len());\n\n        let mut mime_type_strings: Vec<String> = mime_types[..limit]\n            .iter()\n            .map(|(mime, count)| format!(\"{} ({})\", mime, count))\n            .collect();\n\n        if mime_types.len() > limit {\n            mime_type_strings.push(\"...\".to_string());\n        }\n\n        details = details.push(widget::text::body(fl!(\n            \"type\",\n            mime = mime_type_strings.join(\", \")\n        )));\n\n        let size = {\n            if calculating_dir_size {\n                fl!(\"calculating\")\n            } else if let Some(error) = dir_size_error {\n                error\n            } else {\n                format_size(total_size)\n            }\n        };\n\n        details = details.push(widget::text::body(fl!(\"item-size\", size = size)));\n\n        column = column.push(details);\n\n        column = column.push(widget::button::standard(fl!(\"open\")).on_press(Message::Open(None)));\n\n        let mut settings = Vec::new();\n        // Only allow modifying open-with if all mime types are the same\n        if mime_types.len() == 1\n            && let Some(mime) = mime_types\n                .first()\n                .and_then(|(mime, _)| mime.parse::<Mime>().ok())\n            && let Some(mime_app_cache) = mime_app_cache_opt\n        {\n            let mime_apps = mime_app_cache.get(&mime);\n            if !mime_apps.is_empty() {\n                let mime_closure = mime.clone();\n                settings.push(\n                    widget::settings::item::builder(fl!(\"open-with\")).control(\n                        Element::from(\n                            widget::dropdown(\n                                mime_apps,\n                                mime_apps.iter().position(|x| x.is_default),\n                                move |index| (index, mime_closure.clone()),\n                            )\n                            .icons(Cow::Borrowed(mime_app_cache.icons(&mime))),\n                        )\n                        .map(|(index, mime)| {\n                            let mime_app = &mime_apps[index];\n                            Message::SetOpenWith(mime, mime_app.id.clone())\n                        }),\n                    ),\n                );\n            }\n        }\n\n        #[cfg(unix)]\n        {\n            // Only return mode part if it's the only one\n            fn selected_mode_part(mut modes: BTreeSet<u32>) -> Option<usize> {\n                match (modes.pop_first(), modes.pop_first()) {\n                    (Some(mode), None) => Some(mode.try_into().unwrap()),\n                    _ => None,\n                }\n            }\n\n            // Convert a limited number of values from a set into a comma separated list\n            fn join_set(set: BTreeSet<String>) -> String {\n                let limit = 5;\n                let mut title = set.into_iter().collect::<Vec<String>>();\n                if title.len() > limit {\n                    title.truncate(limit);\n                    title.push(\"...\".to_string());\n                }\n                title.join(\", \")\n            }\n\n            let mode_part_user = selected_mode_part(mode_user);\n            settings.push(\n                widget::settings::item::builder(join_set(user_name))\n                    .description(fl!(\"owner\"))\n                    .control(\n                        widget::dropdown(\n                            Cow::Borrowed(MODE_NAMES.as_slice()),\n                            mode_part_user,\n                            move |selected| {\n                                Message::ShiftPermissions(\n                                    None,\n                                    MODE_SHIFT_USER,\n                                    selected.try_into().unwrap(),\n                                )\n                            },\n                        )\n                        .placeholder(fl!(\"mixed\")),\n                    ),\n            );\n\n            let mode_part_group = selected_mode_part(mode_group);\n            settings.push(\n                widget::settings::item::builder(join_set(group_name))\n                    .description(fl!(\"group\"))\n                    .control(\n                        widget::dropdown(\n                            Cow::Borrowed(MODE_NAMES.as_slice()),\n                            mode_part_group,\n                            move |selected| {\n                                Message::ShiftPermissions(\n                                    None,\n                                    MODE_SHIFT_GROUP,\n                                    selected.try_into().unwrap(),\n                                )\n                            },\n                        )\n                        .placeholder(fl!(\"mixed\")),\n                    ),\n            );\n\n            let mode_part_other = selected_mode_part(mode_other);\n            settings.push(\n                widget::settings::item::builder(fl!(\"other\")).control(\n                    widget::dropdown(\n                        Cow::Borrowed(MODE_NAMES.as_slice()),\n                        mode_part_other,\n                        move |selected| {\n                            Message::ShiftPermissions(\n                                None,\n                                MODE_SHIFT_OTHER,\n                                selected.try_into().unwrap(),\n                            )\n                        },\n                    )\n                    .placeholder(fl!(\"mixed\")),\n                ),\n            );\n        }\n\n        if !settings.is_empty() {\n            let mut section = widget::settings::section();\n            section = section.extend(settings);\n            column = column.push(section);\n        }\n\n        column.into()\n    }\n    pub fn view<'a>(\n        &'a self,\n        key_binds: &'a HashMap<KeyBind, Action>,\n        modifiers: &'a Modifiers,\n        clipboard_paste_available: bool,\n        context_actions: &'a [ContextActionPreset],\n    ) -> Element<'a, Message> {\n        widget::responsive(move |size| {\n            widget::id_container(\n                self.view_responsive(\n                    key_binds,\n                    modifiers,\n                    size,\n                    clipboard_paste_available,\n                    context_actions,\n                ),\n                Id::new(format!(\n                    \"tab-{}-{}\",\n                    self.scrollable_id, self.location_title\n                )),\n            )\n            .into()\n        })\n        .into()\n    }\n\n    pub fn subscription(&self, preview: bool) -> Subscription<Message> {\n        //TODO: how many thumbnail loads should be in flight at once?\n        let jobs = self.thumb_config.jobs.get() as usize;\n        let mut subscriptions = Vec::with_capacity(jobs + 3);\n\n        if let Some(items) = &self.items_opt {\n            //TODO: move to function\n            let visible_rect = {\n                let point = match self.scroll_opt {\n                    Some(offset) => Point::new(0.0, offset.y),\n                    None => Point::new(0.0, 0.0),\n                };\n                let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));\n                Rectangle::new(point, size)\n            };\n\n            for item in items {\n                if item.thumbnail_opt.is_some() {\n                    // Skip items that already have a mime type and thumbnail\n                    continue;\n                }\n\n                match item.rect_opt.get() {\n                    Some(rect) => {\n                        if !rect.intersects(&visible_rect) {\n                            // Skip items that are not visible\n                            continue;\n                        }\n                    }\n                    None => {\n                        // Skip items with no determined rect (this should include hidden items)\n                        continue;\n                    }\n                }\n\n                let Some(path) = item.path_opt().cloned() else {\n                    continue;\n                };\n\n                let metadata = item.metadata.clone();\n                let can_thumbnail = match metadata {\n                    ItemMetadata::Path { .. } => true,\n                    #[cfg(feature = \"gvfs\")]\n                    ItemMetadata::GvfsPath { .. } => true,\n                    _ => false,\n                };\n                if can_thumbnail {\n                    let mime = item.mime.clone();\n                    let max_jobs = jobs;\n                    let max_mb = u64::from(self.thumb_config.max_mem_mb.get());\n                    let max_size = u64::from(self.thumb_config.max_size_mb.get());\n\n                    // Determine effective memory budget based on image size\n                    let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {\n                        match item.image_dimensions {\n                            Some((width, height)) => {\n                                let (_use_dedicated, eff_mb, eff_jobs) =\n                                    should_use_dedicated_worker(width, height, max_mb, max_jobs);\n                                (eff_mb, eff_jobs)\n                            }\n                            None => (max_mb, max_jobs),\n                        }\n                    } else {\n                        (max_mb, max_jobs)\n                    };\n\n                    #[derive(Clone)]\n                    struct Wrapper {\n                        path: PathBuf,\n                        metadata: ItemMetadata,\n                        mime: mime::Mime,\n                        effective_max_mb: u64,\n                        effective_jobs: usize,\n                        max_size: u64,\n                    }\n\n                    impl Hash for Wrapper {\n                        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n                            self.path.hash(state);\n                        }\n                    }\n\n                    subscriptions.push(Subscription::run_with(\n                        Wrapper {\n                            path: path.clone(),\n                            metadata,\n                            mime,\n                            effective_max_mb,\n                            effective_jobs,\n                            max_size,\n                        },\n                        |wrapper| {\n                            let Wrapper {\n                                path,\n                                metadata,\n                                mime,\n                                effective_max_mb,\n                                effective_jobs,\n                                max_size,\n                            } = wrapper.clone();\n                            stream::channel(\n                                1,\n                                move |mut output: futures::channel::mpsc::Sender<_>| async move {\n                                    while crate::operation::is_actively_writing_to(&path) {\n                                        crate::operation::actively_writing_tick().await;\n                                    }\n\n                                    let message = {\n                                        let path = path.clone();\n\n                                        // Acquire semaphore permit\n                                        _ = THUMB_SEMAPHORE.acquire().await;\n\n                                        tokio::task::spawn_blocking(move || {\n                                            let start = Instant::now();\n                                            let thumbnail = ItemThumbnail::new(\n                                                &path,\n                                                metadata,\n                                                mime,\n                                                THUMBNAIL_SIZE,\n                                                effective_max_mb,\n                                                effective_jobs,\n                                                max_size,\n                                            );\n                                            log::debug!(\n                                                \"thumbnailed {} in {:?}\",\n                                                path.display(),\n                                                start.elapsed()\n                                            );\n                                            Message::Thumbnail(path, thumbnail)\n                                        })\n                                        .await\n                                        .unwrap()\n                                    };\n\n                                    match output.send(message).await {\n                                        Ok(()) => {}\n                                        Err(err) => {\n                                            log::warn!(\n                                                \"failed to send thumbnail for {}: {}\",\n                                                path.display(),\n                                                err\n                                            );\n                                        }\n                                    }\n\n                                    std::future::pending().await\n                                },\n                            )\n                        },\n                    ));\n                }\n\n                if subscriptions.len() >= jobs {\n                    break;\n                }\n            }\n\n            if preview {\n                // Load directory size for selected items\n\n                let mut selected_items: Vec<&Item> =\n                    items.iter().filter(|item| item.selected).collect();\n\n                if selected_items.is_empty()\n                    && let Some(p) = self.parent_item_opt.as_ref()\n                {\n                    selected_items.push(p)\n                }\n                for item in selected_items {\n                    // Item must have a path\n                    if let Some(path) = item.path_opt().cloned() {\n                        // Item must be calculating directory size\n                        if let DirSize::Calculating(controller) = &item.dir_size {\n                            struct Wrapper {\n                                path: PathBuf,\n                                controller: Controller,\n                            }\n                            impl Hash for Wrapper {\n                                fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n                                    self.path.hash(state);\n                                }\n                            }\n                            subscriptions.push(Subscription::run_with(\n                                Wrapper { path: path.clone(), controller: controller.clone() },\n                                |Wrapper { path, controller }| {\n                                    let path = path.clone();\n                                    let controller = controller.clone();\n                                    stream::channel(1, |mut output: futures::channel::mpsc::Sender<_>| async move {\n                                        let message = {\n                                            let start = Instant::now();\n                                            match calculate_dir_size(&path, controller).await {\n                                                Ok(size) => {\n                                                    log::debug!(\n                                                        \"calculated directory size of {} in {:?}\",\n                                                        path.display(),\n                                                        start.elapsed()\n                                                    );\n                                                    Message::DirectorySize(\n                                                        path.clone(),\n                                                        DirSize::Directory(size),\n                                                    )\n                                                }\n                                                Err(err) => {\n                                                    log::warn!(\n                                                        \"failed to calculate directory size of {}: {}\",\n                                                        path.display(),\n                                                        err\n                                                    );\n                                                    Message::DirectorySize(\n                                                        path.clone(),\n                                                        DirSize::Error(err.to_string()),\n                                                    )\n                                                }\n                                            }\n                                        };\n\n                                        match output.send(message).await {\n                                            Ok(()) => {}\n                                            Err(err) => {\n                                                log::warn!(\n                                                    \"failed to send directory size for {}: {}\",\n                                                    path.display(),\n                                                    err\n                                                );\n                                            }\n                                        }\n\n                                        std::future::pending().await\n                                    })\n                                }\n                            ));\n                        }\n                    }\n                }\n            }\n        }\n\n        // Load search items incrementally\n        if let Location::Search(search_location, term, show_hidden, start) = &self.location {\n            let location = self.location.clone();\n            let search_location = search_location.clone();\n            let term = term.clone();\n            let show_hidden = *show_hidden;\n            let start = *start;\n            #[derive(Debug, Hash, Clone)]\n            struct Wrapper {\n                location: Location,\n                search_location: SearchLocation,\n                term: String,\n                show_hidden: bool,\n                start: Instant,\n            }\n\n            subscriptions.push(Subscription::run_with(\n                Wrapper {\n                    location: location.clone(),\n                    search_location: search_location.clone(),\n                    term: term.clone(),\n                    show_hidden,\n                    start,\n                },\n                |wrapper| {\n                    let wrapper = wrapper.clone();\n                    stream::channel(\n                        2,\n                        move |mut output: futures::channel::mpsc::Sender<Message>| async move {\n                            let Wrapper {\n                                location,\n                                search_location,\n                                term,\n                                show_hidden,\n                                start,\n                            } = wrapper;\n                            //TODO: optimal size?\n                            let (results_tx, results_rx) = mpsc::channel(65536);\n\n                            let ready = Arc::new(atomic::AtomicBool::new(false));\n                            let last_modified_opt = Arc::new(RwLock::new(None));\n                            output\n                                .send(Message::SearchContext(\n                                    location.clone(),\n                                    SearchContextWrapper(Some(SearchContext {\n                                        results_rx,\n                                        ready: ready.clone(),\n                                        last_modified_opt: last_modified_opt.clone(),\n                                    })),\n                                ))\n                                .await\n                                .unwrap();\n\n                            let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true);\n                            {\n                                tokio::task::spawn_blocking(move || {\n                                    scan_search(\n                                        &search_location,\n                                        &term,\n                                        show_hidden,\n                                        move |search_item| -> bool {\n                                            // Don't send if the result is too old\n                                            if let Some(last_modified) =\n                                                *last_modified_opt.read().unwrap()\n                                                && let SearchItem::Path(_, _, ref metadata) =\n                                                    search_item\n                                            {\n                                                if let Ok(modified) = metadata.modified() {\n                                                    if modified < last_modified {\n                                                        return true;\n                                                    }\n                                                } else {\n                                                    return true;\n                                                }\n                                            }\n\n                                            match results_tx.blocking_send(search_item) {\n                                                Ok(()) => {\n                                                    if ready.swap(true, atomic::Ordering::SeqCst) {\n                                                        true\n                                                    } else {\n                                                        // Wake up update method\n                                                        watch_tx.send(false).is_ok()\n                                                    }\n                                                }\n                                                Err(_) => false,\n                                            }\n                                        },\n                                    );\n                                    log::info!(\n                                        \"searched for {:?} in {} in {:?}\",\n                                        term,\n                                        search_location,\n                                        start.elapsed(),\n                                    );\n                                });\n                            }\n\n                            while watch_rx.changed().await.is_ok() {\n                                let is_ready = *watch_rx.borrow_and_update();\n                                let _ = output.send(Message::SearchReady(is_ready)).await;\n                            }\n\n                            // Send final ready\n                            let _ = output.send(Message::SearchReady(true)).await;\n\n                            std::future::pending().await\n                        },\n                    )\n                },\n            ));\n        }\n\n        if let Some(path) = self\n            .edit_location\n            .as_ref()\n            .and_then(|x| x.location.path_opt())\n            .cloned()\n        {\n            subscriptions.push(Subscription::run_with(\n                (\"tab_complete\", path.clone()),\n                |(_, path)| {\n                    let path = path.clone();\n                    stream::channel(\n                        1,\n                        |mut output: futures::channel::mpsc::Sender<_>| async move {\n                            let message = {\n                                let path = path.clone();\n                                tokio::task::spawn_blocking(move || {\n                                    let start = Instant::now();\n                                    match tab_complete(&path) {\n                                        Ok(completions) => {\n                                            log::info!(\n                                                \"tab completed {} in {:?}\",\n                                                path.display(),\n                                                start.elapsed()\n                                            );\n                                            Message::TabComplete(path.clone(), completions)\n                                        }\n                                        Err(err) => {\n                                            log::warn!(\n                                                \"failed to tab complete {}: {}\",\n                                                path.display(),\n                                                err\n                                            );\n                                            Message::TabComplete(path.clone(), Vec::new())\n                                        }\n                                    }\n                                })\n                                .await\n                                .unwrap()\n                            };\n\n                            match output.send(message).await {\n                                Ok(()) => {}\n                                Err(err) => {\n                                    log::warn!(\n                                        \"failed to send tab completion for {}: {}\",\n                                        path.display(),\n                                        err\n                                    );\n                                }\n                            }\n\n                            std::future::pending().await\n                        },\n                    )\n                },\n            ));\n        }\n\n        Subscription::batch(subscriptions)\n    }\n\n    const fn format_time(&self, time: SystemTime) -> FormatTime<'_> {\n        format_time(time, &self.date_time_formatter, &self.time_formatter)\n    }\n}\n\npub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> Option<Message> {\n    if !modifiers.control() {\n        return None;\n    }\n\n    let delta_y = match delta {\n        ScrollDelta::Lines { y, .. } => y,\n        ScrollDelta::Pixels { y, .. } => y,\n    };\n\n    if delta_y > 0.0 {\n        return Some(Message::ZoomIn);\n    }\n\n    if delta_y < 0.0 {\n        return Some(Message::ZoomOut);\n    }\n\n    None\n}\n\nfn text_editor_class(\n    theme: &cosmic::Theme,\n    status: cosmic::widget::text_editor::Status,\n) -> cosmic::iced::widget::text_editor::Style {\n    let cosmic = theme.cosmic();\n    let container = theme.current_container();\n\n    let mut background: cosmic::iced::Color = container.component.base.into();\n    background.a = 0.25;\n    let selection = cosmic.accent.base.into();\n    let value = cosmic.palette.neutral_9.into();\n    let mut placeholder = cosmic.palette.neutral_9;\n    placeholder.alpha = 0.7;\n    let placeholder = placeholder.into();\n\n    match status {\n        cosmic::iced::widget::text_editor::Status::Active\n        | cosmic::iced::widget::text_editor::Status::Disabled => {\n            cosmic::iced::widget::text_editor::Style {\n                background: background.into(),\n                border: cosmic::iced::Border {\n                    radius: cosmic.corner_radii.radius_m.into(),\n                    width: 2.0,\n                    color: container.component.divider.into(),\n                },\n                placeholder,\n                value,\n                selection,\n            }\n        }\n        cosmic::iced::widget::text_editor::Status::Hovered\n        | cosmic::iced::widget::text_editor::Status::Focused { .. } => {\n            cosmic::iced::widget::text_editor::Style {\n                background: background.into(),\n                border: cosmic::iced::Border {\n                    radius: cosmic.corner_radii.radius_m.into(),\n                    width: 2.0,\n                    color: cosmic::iced::Color::from(cosmic.accent.base),\n                },\n                placeholder,\n                value,\n                selection,\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n    use std::{fs, io};\n\n    use cosmic::iced::mouse::ScrollDelta;\n    use cosmic::iced::runtime::keyboard::Modifiers;\n    use cosmic::widget;\n    use log::{debug, trace};\n    use tempfile::TempDir;\n    use test_log::test;\n\n    use super::{Location, Message, Tab, respond_to_scroll_direction, scan_path};\n    use crate::app::test_utils::{\n        NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, assert_eq_tab_path, empty_fs,\n        eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new,\n    };\n    use crate::config::{IconSizes, TabConfig, ThumbCfg};\n\n    // Boilerplate for tab tests. Checks if simulated clicks selected items.\n    fn tab_selects_item(\n        clicks: &[usize],\n        modifiers: Modifiers,\n        expected_selected: &[bool],\n    ) -> io::Result<()> {\n        let (_fs, mut tab) = tab_click_new(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n\n        // Simulate clicks by triggering Message::Click\n        for &click in clicks {\n            debug!(\"Emitting Message::Click(Some({click})) with modifiers: {modifiers:?}\");\n            tab.update(Message::Click(Some(click)), modifiers);\n        }\n\n        let items = tab\n            .items_opt\n            .as_deref()\n            .expect(\"tab should be populated with items\");\n\n        for (i, (&expected, actual)) in expected_selected.iter().zip(items).enumerate() {\n            assert_eq!(\n                expected,\n                actual.selected,\n                \"expected index {i} to be {}\",\n                if expected {\n                    \"selected but it was deselected\"\n                } else {\n                    \"deselected but it was selected\"\n                }\n            );\n        }\n\n        Ok(())\n    }\n\n    fn tab_history() -> io::Result<(TempDir, Tab, Vec<PathBuf>)> {\n        let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n        let mut tab = Tab::new(\n            Location::Path(path.into()),\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n\n        // All directories (simple_fs only produces one nested layer)\n        let dirs: Vec<PathBuf> = {\n            let top_level = filter_dirs(path)?;\n            let mut result = Vec::new();\n            for dir in top_level {\n                let nested_dirs = filter_dirs(&dir)?;\n                result.push(dir);\n                result.extend(nested_dirs);\n            }\n            result\n        };\n        assert!(\n            dirs.len() == NUM_DIRS + NUM_DIRS * NUM_NESTED,\n            \"Sanity check: Have {} dirs instead of {}\",\n            dirs.len(),\n            NUM_DIRS + NUM_DIRS * NUM_NESTED\n        );\n\n        debug!(\"Building history by emitting Message::Location\");\n        for dir in &dirs {\n            debug!(\n                \"Emitting Message::Location(Location::Path(\\\"{}\\\"))\",\n                dir.display()\n            );\n            tab.update(\n                Message::Location(Location::Path(dir.clone())),\n                Modifiers::empty(),\n            );\n        }\n        trace!(\"Tab history: {:?}\", tab.history);\n\n        Ok((fs, tab, dirs))\n    }\n\n    #[test]\n    fn scan_path_succeeds_on_valid_path() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        // Read directory entries and sort as cosmic-files does\n        let entries = read_dir_sorted(path)?;\n\n        debug!(\"Calling scan_path(\\\"{}\\\")\", path.display());\n        let actual = scan_path(&path.to_owned(), IconSizes::default());\n\n        // scan_path shouldn't skip any entries\n        assert_eq!(entries.len(), actual.len());\n\n        // Correct files should be scanned\n        assert!(\n            entries\n                .into_iter()\n                .zip(actual.into_iter())\n                .all(|(path, item)| eq_path_item(&path, &item))\n        );\n\n        Ok(())\n    }\n\n    #[test]\n    fn scan_path_returns_empty_vec_for_invalid_path() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        // A nonexisting path within the temp dir\n        let invalid_path = path.join(\"ferris\");\n        assert!(!invalid_path.exists());\n\n        debug!(\"Calling scan_path(\\\"{}\\\")\", invalid_path.display());\n        let actual = scan_path(&invalid_path, IconSizes::default());\n\n        assert!(actual.is_empty());\n\n        Ok(())\n    }\n\n    #[test]\n    fn scan_path_empty_dir_returns_empty_vec() -> io::Result<()> {\n        let fs = empty_fs()?;\n        let path = fs.path();\n\n        debug!(\"Calling scan_path(\\\"{}\\\")\", path.display());\n        let actual = scan_path(&path.to_owned(), IconSizes::default());\n\n        assert_eq!(0, path.read_dir()?.count());\n        assert!(actual.is_empty());\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_location_changes_location() -> io::Result<()> {\n        let fs = simple_fs(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        // Next directory in temp directory\n        // This does not have to be sorted\n        let next_dir = filter_dirs(path)?\n            .next()\n            .expect(\"temp directory should have at least one directory\");\n\n        let mut tab = Tab::new(\n            Location::Path(path.to_owned()),\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n        debug!(\n            \"Emitting Message::Location(Location::Path(\\\"{}\\\"))\",\n            next_dir.display()\n        );\n        tab.update(\n            Message::Location(Location::Path(next_dir.clone())),\n            Modifiers::empty(),\n        );\n\n        // Validate that the tab's path updated\n        // NOTE: `items_opt` is set to None with Message::Location so this ONLY checks for equal paths\n        // If item contents are NOT None then this needs to be reevaluated for correctness\n        assert_eq_tab_path(&tab, &next_dir);\n        assert!(\n            tab.items_opt.is_none(),\n            \"Tab's `items` is not None which means this test needs to be updated\"\n        );\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_click_single_selects_item() -> io::Result<()> {\n        // Select the second directory with no keys held down\n        tab_selects_item(&[1], Modifiers::empty(), &[false, true])\n    }\n\n    #[test]\n    fn tab_click_double_opens_folder() -> io::Result<()> {\n        let (fs, mut tab) = tab_click_new(NUM_FILES, NUM_NESTED, NUM_DIRS, NUM_NESTED, NAME_LEN)?;\n        let path = fs.path();\n\n        // Simulate double clicking second directory\n        debug!(\"Emitting double click Message::DoubleClick(Some(1))\");\n        tab.update(Message::DoubleClick(Some(1)), Modifiers::empty());\n\n        // Path to second directory\n        let second_dir = read_dir_sorted(path)?\n            .into_iter()\n            .filter(|p| p.is_dir())\n            .nth(1)\n            .expect(\"should be at least two directories\");\n\n        // Location should have changed to second_dir\n        assert_eq_tab_path(&tab, &second_dir);\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_click_ctrl_selects_multiple() -> io::Result<()> {\n        // Select the first and second directory by holding down ctrl\n        tab_selects_item(&[0, 1], Modifiers::CTRL, &[true, true])\n    }\n\n    #[test]\n    fn tab_gonext_moves_forward_in_history() -> io::Result<()> {\n        let (fs, mut tab, dirs) = tab_history()?;\n        let path = fs.path();\n\n        // Rewind to the start\n        for _ in 0..dirs.len() {\n            debug!(\"Emitting Message::GoPrevious to rewind to the start\",);\n            tab.update(Message::GoPrevious, Modifiers::empty());\n        }\n        assert_eq_tab_path(&tab, path);\n\n        // Back to the future. Directories should be in the order they were opened.\n        for dir in dirs {\n            debug!(\"Emitting Message::GoNext\",);\n            tab.update(Message::GoNext, Modifiers::empty());\n            assert_eq_tab_path(&tab, &dir);\n        }\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_goprev_moves_backward_in_history() -> io::Result<()> {\n        let (fs, mut tab, dirs) = tab_history()?;\n        let path = fs.path();\n\n        for dir in dirs.into_iter().rev() {\n            assert_eq_tab_path(&tab, &dir);\n            debug!(\"Emitting Message::GoPrevious\",);\n            tab.update(Message::GoPrevious, Modifiers::empty());\n        }\n        assert_eq_tab_path(&tab, path);\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> {\n        let message_maybe =\n            respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, &Modifiers::CTRL);\n        assert!(message_maybe.is_some());\n        assert!(matches!(message_maybe.unwrap(), Message::ZoomIn));\n        Ok(())\n    }\n\n    #[test]\n    fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {\n        let message_maybe = respond_to_scroll_direction(\n            ScrollDelta::Pixels { x: 0.0, y: 1.0 },\n            &Modifiers::empty(),\n        );\n        assert!(message_maybe.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> {\n        let message_maybe =\n            respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, &Modifiers::CTRL);\n        assert!(message_maybe.is_some());\n        assert!(matches!(message_maybe.unwrap(), Message::ZoomOut));\n        Ok(())\n    }\n\n    #[test]\n    fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {\n        let message_maybe = respond_to_scroll_direction(\n            ScrollDelta::Pixels { x: 0.0, y: -1.0 },\n            &Modifiers::empty(),\n        );\n        assert!(message_maybe.is_none());\n        Ok(())\n    }\n    #[test]\n    fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> {\n        let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;\n        let path = fs.path();\n        let mut tab = Tab::new(\n            Location::Path(path.into()),\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n\n        // Tab's location shouldn't change if GoPrev or GoNext is triggered\n        debug!(\"Emitting Message::GoPrevious\",);\n        tab.update(Message::GoPrevious, Modifiers::empty());\n        assert_eq_tab_path(&tab, path);\n\n        debug!(\"Emitting Message::GoNext\",);\n        tab.update(Message::GoNext, Modifiers::empty());\n        assert_eq_tab_path(&tab, path);\n\n        Ok(())\n    }\n\n    #[test]\n    fn tab_locationup_moves_up_hierarchy() -> io::Result<()> {\n        let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;\n        let path = fs.path();\n        let mut next_dir = filter_dirs(path)?\n            .next()\n            .expect(\"should be at least one directory\");\n\n        let mut tab = Tab::new(\n            Location::Path(next_dir.clone()),\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n        // This will eventually yield false once root is hit\n        while next_dir.pop() {\n            debug!(\"Emitting Message::LocationUp\",);\n            tab.update(Message::LocationUp, Modifiers::empty());\n            assert_eq_tab_path(&tab, &next_dir);\n        }\n\n        Ok(())\n    }\n\n    #[test]\n    fn sort_long_number_file_names() -> io::Result<()> {\n        let fs = empty_fs()?;\n        let path = fs.path();\n\n        // Create files with names 255 characters long that only contain a single number\n        // Example: 0000...0 for 255 characters\n        // https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations\n        let mut base_nums: Vec<_> = ('0'..='9').collect();\n        fastrand::shuffle(&mut base_nums);\n        debug!(\"Shuffled numbers for paths: {base_nums:?}\");\n        let paths: Vec<_> = base_nums\n            .iter()\n            .copied()\n            .map(|base| path.join(std::iter::repeat_n(base, 255).collect::<String>()))\n            .collect();\n\n        for (file, base) in paths.iter().zip(base_nums.into_iter()) {\n            trace!(\"Creating long file name for {base}\");\n            fs::File::create(file)?;\n        }\n\n        debug!(\"Creating tab for directory of long file names\");\n        Tab::new(\n            Location::Path(path.into()),\n            TabConfig::default(),\n            ThumbCfg::default(),\n            None,\n            widget::Id::unique(),\n            None,\n        );\n\n        Ok(())\n    }\n\n    #[test]\n    fn mode_calculations() {\n        use super::{\n            MODE_SHIFT_GROUP, MODE_SHIFT_OTHER, MODE_SHIFT_USER, get_mode_part, set_mode_part,\n        };\n        for user in 0..=7 {\n            for group in 0..=7 {\n                for other in 0..=7 {\n                    let mode = (user << MODE_SHIFT_USER)\n                        | (group << MODE_SHIFT_GROUP)\n                        | (other << MODE_SHIFT_OTHER);\n                    assert_eq!(format!(\"{mode:03o}\"), format!(\"{user:o}{group:o}{other:o}\"),);\n                    assert_eq!(get_mode_part(mode, MODE_SHIFT_USER), user);\n                    assert_eq!(get_mode_part(mode, MODE_SHIFT_GROUP), group);\n                    assert_eq!(get_mode_part(mode, MODE_SHIFT_OTHER), other);\n\n                    let mode_no_user = (group << MODE_SHIFT_GROUP) | (other << MODE_SHIFT_OTHER);\n                    assert_eq!(\n                        format!(\"{mode_no_user:03o}\"),\n                        format!(\"0{group:o}{other:o}\")\n                    );\n                    assert_eq!(set_mode_part(mode_no_user, MODE_SHIFT_USER, user), mode);\n\n                    let mode_no_group = (user << MODE_SHIFT_USER) | (other << MODE_SHIFT_OTHER);\n                    assert_eq!(\n                        format!(\"{mode_no_group:03o}\"),\n                        format!(\"{user:o}0{other:o}\")\n                    );\n                    assert_eq!(set_mode_part(mode_no_group, MODE_SHIFT_GROUP, group), mode);\n\n                    let mode_no_other = (user << MODE_SHIFT_USER) | (group << MODE_SHIFT_GROUP);\n                    assert_eq!(\n                        format!(\"{mode_no_other:03o}\"),\n                        format!(\"{user:o}{group:o}0\")\n                    );\n                    assert_eq!(set_mode_part(mode_no_other, MODE_SHIFT_OTHER, other), mode);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/thumbnail_cacher.rs",
    "content": "use image::DynamicImage;\nuse md5::{Digest, Md5};\nuse rustc_hash::FxHashMap;\nuse std::error::Error;\nuse std::fs::{self, File};\nuse std::io::{self, BufReader, BufWriter};\n#[cfg(unix)]\nuse std::os::unix::fs::PermissionsExt;\nuse std::path::{Path, PathBuf};\nuse std::sync::LazyLock;\nuse std::time::UNIX_EPOCH;\nuse tempfile::NamedTempFile;\nuse url::Url;\n\n/// Implements thumbnail caching based on the freedesktop.org Thumbnail Managing Standard.\n/// <https://specifications.freedesktop.org/thumbnail-spec/latest>/\npub struct ThumbnailCacher {\n    file_path: PathBuf,\n    file_uri: String,\n    thumbnail_dir: PathBuf,\n    thumbnail_path: PathBuf,\n    thumbnail_size: ThumbnailSize,\n    thumbnail_fail_marker_path: PathBuf,\n}\n\nimpl ThumbnailCacher {\n    pub fn new(file_path: &Path, thumbnail_size: ThumbnailSize) -> Result<Self, String> {\n        let file_uri = thumbnail_uri(file_path)\n            .map_err(|err| format!(\"failed to create URI for {}: {}\", file_path.display(), err))?;\n        let cache_base_dir = THUMBNAIL_CACHE_BASE_DIR\n            .as_ref()\n            .ok_or(\"failed to get thumbnail cache directory\".to_string())?;\n        let thumbnail_filename = thumbnail_cache_filename(&file_uri);\n        let thumbnail_dir = cache_base_dir.join(thumbnail_size.subdirectory_name());\n        if !thumbnail_dir.is_dir() {\n            log::warn!(\n                \"{} is not a directory, creating one now\",\n                thumbnail_dir.display()\n            );\n            let _: () = log::error!(\n                \"{} failed to create directory, this error can be expected on first run\",\n                thumbnail_dir.display()\n            );\n            fs::create_dir_all(&thumbnail_dir).unwrap_or(());\n        }\n        let thumbnail_path = thumbnail_dir.join(&thumbnail_filename);\n        let thumbnail_fail_marker_path = cache_base_dir\n            .join(\"fail\")\n            .join(format!(\"cosmic-files-{}\", env!(\"CARGO_PKG_VERSION\")))\n            .join(&thumbnail_filename);\n\n        Ok(Self {\n            file_path: file_path.to_path_buf(),\n            file_uri,\n            thumbnail_dir,\n            thumbnail_path,\n            thumbnail_size,\n            thumbnail_fail_marker_path,\n        })\n    }\n\n    pub fn get_cached_thumbnail(&self) -> CachedThumbnail {\n        // If the file is already a thumbnail, just use it so we don't generate\n        // cached thumbnails of thumbnails.\n        if let (Some(cache_base_dir), Ok(metadata)) = (\n            THUMBNAIL_CACHE_BASE_DIR.as_ref(),\n            std::fs::metadata(&self.file_path),\n        ) && metadata.is_file()\n            && self.file_path.starts_with(cache_base_dir)\n        {\n            return CachedThumbnail::Valid((self.file_path.clone(), None));\n        }\n\n        // Use cached thumbnail if it is valid.\n        if self.is_thumbnail_valid(&self.thumbnail_path) {\n            return CachedThumbnail::Valid((\n                self.thumbnail_path.clone(),\n                Some(self.thumbnail_size),\n            ));\n        }\n\n        // Check if there is a fail marker from an earlier failure.\n        if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) {\n            return CachedThumbnail::Failed;\n        }\n\n        CachedThumbnail::RequiresUpdate(self.thumbnail_size)\n    }\n\n    pub fn thumbnail_dir(&self) -> &Path {\n        &self.thumbnail_dir\n    }\n\n    pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box<dyn Error>> {\n        #[cfg(unix)]\n        fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?;\n        self.update_thumbnail_text_metadata(temp_file.path())?;\n        fs::rename(temp_file.path(), &self.thumbnail_path)?;\n\n        Ok(&self.thumbnail_path)\n    }\n\n    pub fn update_with_image(&self, image: DynamicImage) -> Result<&Path, Box<dyn Error>> {\n        let temp_file = tempfile::Builder::new()\n            .prefix(\"cosmic-files-\")\n            .tempfile_in(&self.thumbnail_dir)?;\n        {\n            let file = File::create(temp_file.path())?;\n            let image = image\n                .thumbnail(\n                    self.thumbnail_size.pixel_size(),\n                    self.thumbnail_size.pixel_size(),\n                )\n                .into_rgba8();\n            let writer = BufWriter::new(file);\n            let mut encoder = png::Encoder::new(writer, image.width(), image.height());\n            encoder.set_color(png::ColorType::Rgba);\n            encoder.set_depth(png::BitDepth::Eight);\n            encoder\n                .write_header()?\n                .write_image_data(&image.into_raw())?;\n        }\n\n        self.update_with_temp_file(temp_file)\n    }\n\n    pub fn create_fail_marker(&self) -> Result<(), Box<dyn Error>> {\n        if let Some(dir) = self.thumbnail_fail_marker_path.parent() {\n            fs::create_dir_all(dir)?;\n            #[cfg(unix)]\n            fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;\n        }\n\n        let file = File::create(&self.thumbnail_fail_marker_path)?;\n        let writer = BufWriter::new(file);\n        let mut encoder = png::Encoder::new(writer, 1, 1);\n        encoder.set_color(png::ColorType::Grayscale);\n        encoder.set_depth(png::BitDepth::One);\n        encoder.write_header()?.write_image_data(&[0])?;\n        self.update_thumbnail_text_metadata(&self.thumbnail_fail_marker_path)\n    }\n\n    fn update_thumbnail_text_metadata(&self, path: &Path) -> Result<(), Box<dyn Error>> {\n        let file = File::open(path)?;\n        let reader = BufReader::new(file);\n\n        let decoder = png::Decoder::new(reader);\n        let mut reader = decoder.read_info()?;\n        let (width, height, color_type, bit_depth, mut text_chunks) = {\n            let info = reader.info();\n            let text_chunks: FxHashMap<String, String> = info\n                .uncompressed_latin1_text\n                .iter()\n                .map(|chunk| (chunk.keyword.clone(), chunk.text.clone()))\n                .collect();\n            (\n                info.width,\n                info.height,\n                info.color_type,\n                info.bit_depth,\n                text_chunks,\n            )\n        };\n\n        let mut image_data = vec![\n            0;\n            reader\n                .output_buffer_size()\n                .ok_or(\"The required image buffer size is too large.\")?\n        ];\n        reader.next_frame(&mut image_data)?;\n\n        let file = File::create(path)?;\n        let writer = BufWriter::new(file);\n\n        let mut encoder = png::Encoder::new(writer, width, height);\n        encoder.set_color(color_type);\n        encoder.set_depth(bit_depth);\n\n        text_chunks.insert(\"Software\".to_string(), \"COSMIC Files\".to_string());\n        text_chunks.insert(\"Thumb::URI\".to_string(), self.file_uri.clone());\n        let metadata = std::fs::metadata(&self.file_path)?;\n        let size = metadata.len();\n        text_chunks.insert(\"Thumb::Size\".to_string(), size.to_string());\n        let mtime = metadata\n            .modified()?\n            .duration_since(UNIX_EPOCH)\n            .unwrap_or_default()\n            .as_secs();\n        text_chunks.insert(\"Thumb::MTime\".to_string(), mtime.to_string());\n\n        for (keyword, text) in text_chunks {\n            encoder.add_text_chunk(keyword, text)?;\n        }\n\n        let mut writer = encoder.write_header()?;\n        writer.write_image_data(&image_data)?;\n\n        Ok(())\n    }\n\n    fn is_thumbnail_valid(&self, thumbnail_path: &Path) -> bool {\n        let thumbnail_file = match File::open(thumbnail_path) {\n            Ok(file) => file,\n            Err(_) => return false,\n        };\n        let decoder = png::Decoder::new(BufReader::new(thumbnail_file));\n        let reader = match decoder.read_info() {\n            Ok(reader) => reader,\n            Err(err) => {\n                log::warn!(\n                    \"failed to decode {} as PNG: {}\",\n                    thumbnail_path.display(),\n                    err\n                );\n                return false;\n            }\n        };\n\n        let texts = &reader.info().uncompressed_latin1_text;\n\n        // Thumb::URI is required and must match.\n        let thumb_uri = texts\n            .iter()\n            .find(|&text| text.keyword == \"Thumb::URI\")\n            .map(|t| &t.text);\n        if let Some(thumb_uri) = thumb_uri {\n            if *thumb_uri != self.file_uri {\n                return false;\n            }\n        } else {\n            return false;\n        }\n\n        let metadata = match std::fs::metadata(&self.file_path) {\n            Ok(m) => m,\n            Err(err) => {\n                log::warn!(\n                    \"failed to get metatdata of {}: {}\",\n                    self.file_path.display(),\n                    err\n                );\n                return false;\n            }\n        };\n\n        // Thumb::MTime is required and must match.\n        let thumb_mtime = texts\n            .iter()\n            .find(|&text| text.keyword == \"Thumb::MTime\")\n            .map(|t| &t.text);\n        if let Some(thumb_mtime) = thumb_mtime {\n            let modified = match metadata.modified() {\n                Ok(m) => m,\n                Err(err) => {\n                    log::warn!(\n                        \"failed to get modified from metatdata of {}, {}\",\n                        self.file_path.display(),\n                        err\n                    );\n                    return false;\n                }\n            };\n            let mtime = modified\n                .duration_since(UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_secs()\n                .to_string();\n            if *thumb_mtime != mtime {\n                return false;\n            }\n        } else {\n            return false;\n        }\n\n        // Thumb::Size isn't required, but it should be verified if present.\n        let thumb_size = texts\n            .iter()\n            .find(|&text| text.keyword == \"Thumb::Size\")\n            .map(|t| &t.text);\n        if let Some(thumb_size) = thumb_size {\n            let size = metadata.len();\n            if *thumb_size != size.to_string() {\n                return false;\n            }\n        }\n\n        true\n    }\n}\n\nfn thumbnail_uri(path: &Path) -> io::Result<String> {\n    let absolute_path = fs::canonicalize(path)?;\n    let url = Url::from_file_path(&absolute_path).map_err(|()| {\n        io::Error::other(format!(\n            \"failed to create URI for thumbnail_file: {}\",\n            absolute_path.display()\n        ))\n    })?;\n    // Technically square brackets don't need to be percent encoded,\n    // and they aren't by the url crate, but the thumbnailer used by\n    // Gnome Files does. In order to share thumbnails and not get duplicates\n    // we should do the same.\n    let url = url.as_str().replace('[', \"%5B\").replace(']', \"%5D\");\n    Ok(url)\n}\n\nfn thumbnail_cache_filename(file_uri: &str) -> String {\n    let hash = Md5::digest(file_uri);\n    format!(\"{hash:x}.png\")\n}\n\n#[derive(Copy, Clone, Debug, PartialEq, Eq)]\n#[repr(u32)]\npub enum ThumbnailSize {\n    Normal = 128,\n    Large = 256,\n    XLarge = 512,\n    XXLarge = 1024,\n}\n\nimpl ThumbnailSize {\n    pub fn from_pixel_size(pixel_size: u32) -> Self {\n        if pixel_size <= Self::Normal.pixel_size() {\n            Self::Normal\n        } else if pixel_size <= Self::Large.pixel_size() {\n            Self::Large\n        } else if pixel_size <= Self::XLarge.pixel_size() {\n            Self::XLarge\n        } else {\n            Self::XXLarge\n        }\n    }\n\n    pub const fn pixel_size(self) -> u32 {\n        self as u32\n    }\n\n    pub const fn subdirectory_name(self) -> &'static str {\n        match self {\n            Self::Normal => \"normal\",\n            Self::Large => \"large\",\n            Self::XLarge => \"x-large\",\n            Self::XXLarge => \"xx-large\",\n        }\n    }\n}\n\npub enum CachedThumbnail {\n    /// The cached thumbnail is valid and should be used with size if known.\n    Valid((PathBuf, Option<ThumbnailSize>)),\n    /// The cached thumbnail doesn't exist or it's invalid and\n    /// needs to be recreated with the pixel size.\n    RequiresUpdate(ThumbnailSize),\n    // The cached thumbnail is in a failed state.\n    // This means it failed to create by cosmic-files in the past\n    // and shouldn't be tried again.\n    Failed,\n}\n\nstatic THUMBNAIL_CACHE_BASE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| {\n    if let Some(cache_dir) = dirs::cache_dir() {\n        return Some(cache_dir.join(\"thumbnails\"));\n    }\n\n    log::warn!(\"failed to get thumbnail cache directory, thumbnails will not be cached\");\n\n    None\n});\n"
  },
  {
    "path": "src/thumbnailer.rs",
    "content": "// Copyright 2023 System76 <info@system76.com>\n// SPDX-License-Identifier: GPL-3.0-only\n\n#[cfg(feature = \"desktop\")]\nuse cosmic::desktop::fde::GenericEntry;\nuse mime_guess::Mime;\nuse rustc_hash::FxHashMap;\nuse std::path::Path;\nuse std::sync::{LazyLock, Mutex};\nuse std::time::Instant;\nuse std::{fs, process};\n\n#[derive(Clone, Debug)]\npub struct Thumbnailer {\n    pub exec: String,\n}\n\nimpl Thumbnailer {\n    pub fn command(\n        &self,\n        input: &Path,\n        output: &Path,\n        thumbnail_size: u32,\n    ) -> Option<process::Command> {\n        let args_vec: Vec<String> = shlex::split(&self.exec)?;\n        let mut args = args_vec.iter();\n        let mut command = process::Command::new(args.next()?);\n        for arg in args {\n            if arg.starts_with('%') {\n                match arg.as_str() {\n                    \"%i\" | \"%u\" => {\n                        command.arg(input);\n                    }\n                    \"%o\" => {\n                        command.arg(output);\n                    }\n                    \"%s\" => {\n                        command.arg(format!(\"{thumbnail_size}\"));\n                    }\n                    _ => {\n                        log::warn!(\n                            \"unsupported thumbnailer Exec code {:?} in {:?}\",\n                            arg,\n                            self.exec\n                        );\n                        return None;\n                    }\n                }\n            } else {\n                command.arg(arg);\n            }\n        }\n        Some(command)\n    }\n}\n\npub struct ThumbnailerCache {\n    cache: FxHashMap<Mime, Vec<Thumbnailer>>,\n}\n\nimpl ThumbnailerCache {\n    pub fn new() -> Self {\n        let mut thumbnailer_cache = Self {\n            cache: FxHashMap::default(),\n        };\n        thumbnailer_cache.reload();\n        thumbnailer_cache\n    }\n\n    #[cfg(not(feature = \"desktop\"))]\n    pub fn reload(&mut self) {}\n\n    #[cfg(feature = \"desktop\")]\n    pub fn reload(&mut self) {\n        let start = Instant::now();\n\n        self.cache.clear();\n\n        let mut search_dirs = Vec::new();\n        let xdg_dirs = xdg::BaseDirectories::new();\n\n        if let Some(mut data_home) = xdg_dirs.get_data_home() {\n            data_home.push(\"thumbnailers\");\n            search_dirs.push(data_home);\n        }\n        search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| {\n            data_dir.push(\"thumbnailers\");\n            data_dir\n        }));\n\n        let mut thumbnailer_paths = Vec::new();\n        for dir in search_dirs {\n            log::trace!(\"looking for thumbnailers in {}\", dir.display());\n            match fs::read_dir(&dir) {\n                Ok(entries) => {\n                    thumbnailer_paths.extend(entries.filter_map(|entry_res| {\n                        entry_res\n                            .inspect_err(|err| {\n                                log::warn!(\n                                    \"failed to read entry in directory {}: {}\",\n                                    dir.display(),\n                                    err\n                                )\n                            })\n                            .ok()\n                            .map(|entry| entry.path())\n                    }));\n                }\n                Err(err) => {\n                    log::warn!(\"failed to read directory {}: {}\", dir.display(), err);\n                }\n            }\n        }\n\n        //TODO: handle directory specific behavior\n        for path in thumbnailer_paths {\n            let entry = match GenericEntry::from_path(&path) {\n                Ok(ok) => ok,\n                Err(err) => {\n                    log::warn!(\"failed to parse {}: {}\", path.display(), err);\n                    continue;\n                }\n            };\n\n            //TODO: use TryExec?\n            let Some(section) = entry.group(\"Thumbnailer Entry\") else {\n                log::warn!(\n                    \"missing Thumbnailer Entry section for thumbnailer {}\",\n                    path.display()\n                );\n                continue;\n            };\n            let Some(exec) = section.entry(\"Exec\") else {\n                log::warn!(\"missing Exec attribute for thumbnailer {}\", path.display());\n                continue;\n            };\n            let Some(mime_types) = section.entry(\"MimeType\") else {\n                log::warn!(\n                    \"missing MimeType attribute for thumbnailer {}\",\n                    path.display()\n                );\n                continue;\n            };\n\n            for mime_type in mime_types.split_terminator(';') {\n                if let Ok(mime) = mime_type.parse::<Mime>() {\n                    log::trace!(\"thumbnailer {}={}\", mime, path.display());\n                    let apps = self\n                        .cache\n                        .entry(mime)\n                        .or_insert_with(|| Vec::with_capacity(1));\n                    apps.push(Thumbnailer {\n                        exec: exec.to_string(),\n                    });\n                }\n            }\n        }\n\n        let elapsed = start.elapsed();\n        log::info!(\"loaded thumbnailer cache in {elapsed:?}\");\n    }\n\n    pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {\n        self.cache.get(key).map_or_else(Vec::new, Vec::clone)\n    }\n}\n\nstatic THUMBNAILER_CACHE: LazyLock<Mutex<ThumbnailerCache>> =\n    LazyLock::new(|| Mutex::new(ThumbnailerCache::new()));\n\npub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {\n    let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();\n    thumbnailer_cache.get(mime)\n}\n"
  },
  {
    "path": "src/trash.rs",
    "content": "use cosmic::widget;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::PathBuf;\n\nuse crate::config::IconSizes;\nuse crate::tab::{Item, SearchItem};\n\npub trait TrashExt {\n    fn is_empty() -> bool {\n        true\n    }\n\n    fn entries() -> usize {\n        0\n    }\n\n    fn folders() -> Result<HashSet<PathBuf>, trash::Error> {\n        Err(trash::Error::Unknown {\n            description: \"reading trash folders not supported on this platform\".into(),\n        })\n    }\n\n    fn scan(_sizes: IconSizes) -> Vec<Item> {\n        log::warn!(\"viewing trash not supported on this platform\");\n        Vec::new()\n    }\n\n    fn scan_search<F: Fn(SearchItem) -> bool + Sync>(_callback: F, _regex: &Regex) {}\n\n    fn icon(icon_size: u16) -> widget::icon::Handle {\n        widget::icon::from_name(if Self::is_empty() {\n            \"user-trash\"\n        } else {\n            \"user-trash-full\"\n        })\n        .size(icon_size)\n        .handle()\n    }\n\n    fn icon_symbolic(icon_size: u16) -> widget::icon::Handle {\n        widget::icon::from_name(if Self::is_empty() {\n            \"user-trash-symbolic\"\n        } else {\n            \"user-trash-full-symbolic\"\n        })\n        .size(icon_size)\n        .handle()\n    }\n}\n\npub struct Trash;\n\n// This config statement is from trash::os_limited\n#[cfg(any(\n    target_os = \"windows\",\n    all(\n        unix,\n        not(target_os = \"macos\"),\n        not(target_os = \"ios\"),\n        not(target_os = \"android\")\n    )\n))]\nimpl TrashExt for Trash {\n    fn is_empty() -> bool {\n        trash::os_limited::is_empty().unwrap_or(true)\n    }\n\n    fn entries() -> usize {\n        match trash::os_limited::list() {\n            Ok(entries) => entries.len(),\n            Err(_err) => 0,\n        }\n    }\n\n    // Not available on Windows only\n    #[cfg(not(target_os = \"windows\"))]\n    fn folders() -> Result<HashSet<PathBuf>, trash::Error> {\n        trash::os_limited::trash_folders()\n    }\n\n    fn scan(sizes: IconSizes) -> Vec<Item> {\n        use crate::localize::LANGUAGE_SORTER;\n        use crate::tab::item_from_trash_entry;\n        use std::cmp::Ordering;\n\n        let entries = match trash::os_limited::list() {\n            Ok(entry) => entry,\n            Err(err) => {\n                log::warn!(\"failed to read trash items: {err}\");\n                return Vec::new();\n            }\n        };\n        let mut items: Vec<_> = entries\n            .into_iter()\n            .filter_map(|entry| {\n                let metadata = trash::os_limited::metadata(&entry)\n                    .inspect_err(|err| {\n                        log::warn!(\"failed to get metadata for trash item {entry:?}: {err}\")\n                    })\n                    .ok()?;\n                Some(item_from_trash_entry(entry, metadata, sizes))\n            })\n            .collect();\n        items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {\n            (true, false) => Ordering::Less,\n            (false, true) => Ordering::Greater,\n            _ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),\n        });\n        items\n    }\n\n    fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {\n        let entries = match trash::os_limited::list() {\n            Ok(entries) => entries,\n            Err(err) => {\n                log::warn!(\"failed to read trash items: {err}\");\n                return;\n            }\n        };\n\n        for entry in entries {\n            if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {\n                log::warn!(\"failed to get metadata for trash item {entry:?}: {err}\")\n            }) {\n                let name = entry.name.to_string_lossy();\n                if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {\n                    break;\n                }\n            }\n        }\n    }\n}\n\n// This config statement is from trash::os_limited, inverted\n#[cfg(not(any(\n    target_os = \"windows\",\n    all(\n        unix,\n        not(target_os = \"macos\"),\n        not(target_os = \"ios\"),\n        not(target_os = \"android\")\n    )\n)))]\nimpl TrashExt for Trash {}\n"
  },
  {
    "path": "src/zoom.rs",
    "content": "use std::num::NonZeroU16;\n\nuse crate::config::IconSizes;\nuse crate::tab::View;\n\nstatic DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap();\nstatic MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();\nstatic MAX_ZOOM: NonZeroU16 = NonZeroU16::new(500).unwrap();\nconst ZOOM_STEP: u16 = 25;\n\npub(crate) const fn zoom_to_default(view: View, icon_sizes: &mut IconSizes) {\n    let icon_size = select_resized_icon(view, icon_sizes);\n    *icon_size = DEFAULT_ZOOM;\n}\n\npub(crate) fn zoom_in_view(view: View, icon_sizes: &mut IconSizes) {\n    let icon_size = select_resized_icon(view, icon_sizes);\n\n    let mut step = MIN_ZOOM;\n    while step <= MAX_ZOOM {\n        if *icon_size < step {\n            *icon_size = step;\n            break;\n        }\n        step = step.saturating_add(ZOOM_STEP);\n    }\n    if *icon_size > step {\n        *icon_size = step;\n    }\n}\n\npub(crate) fn zoom_out_view(view: View, icon_sizes: &mut IconSizes) {\n    let icon_size = select_resized_icon(view, icon_sizes);\n\n    let mut step = MAX_ZOOM;\n    while step >= MIN_ZOOM {\n        if *icon_size > step {\n            *icon_size = step;\n            break;\n        }\n        step = NonZeroU16::new(step.get().saturating_sub(ZOOM_STEP)).unwrap();\n    }\n    if *icon_size < step {\n        *icon_size = step;\n    }\n}\n\nconst fn select_resized_icon(view: View, icon_sizes: &mut IconSizes) -> &mut NonZeroU16 {\n    match view {\n        View::Grid => &mut icon_sizes.grid,\n        View::List => &mut icon_sizes.list,\n    }\n}\n"
  }
]